feat: implement filename deduplication utility and integrate across multiple file conversion modules
This commit is contained in:
@@ -7,13 +7,18 @@ import {
|
||||
renderFileDisplay,
|
||||
switchView,
|
||||
} from '../ui.js';
|
||||
import { formatIsoDate, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import {
|
||||
formatIsoDate,
|
||||
readFileAsArrayBuffer,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
||||
import {
|
||||
multiFileTools,
|
||||
simpleTools,
|
||||
@@ -21,16 +26,23 @@ import {
|
||||
} from '../config/pdf-tools.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
// Re-export rotation state utilities
|
||||
export { getRotationState, updateRotationState, resetRotationState, initializeRotationState } from '../utils/rotation-state.js';
|
||||
export {
|
||||
getRotationState,
|
||||
updateRotationState,
|
||||
resetRotationState,
|
||||
initializeRotationState,
|
||||
} from '../utils/rotation-state.js';
|
||||
|
||||
const rotationState: number[] = [];
|
||||
let imageSortableInstance: Sortable | null = null;
|
||||
const activeImageUrls = new Map<File, string>();
|
||||
|
||||
|
||||
async function handleSinglePdfUpload(toolId, file) {
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
@@ -107,7 +119,11 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
.toString();
|
||||
}
|
||||
|
||||
if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') {
|
||||
if (
|
||||
toolId === 'organize' ||
|
||||
toolId === 'rotate' ||
|
||||
toolId === 'delete-pages'
|
||||
) {
|
||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||
|
||||
if (toolId === 'rotate') {
|
||||
@@ -124,11 +140,18 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
const rotateAllRightBtn = document.getElementById(
|
||||
'rotate-all-right-btn'
|
||||
);
|
||||
const rotateAllCustomBtn = document.getElementById('rotate-all-custom-btn');
|
||||
const rotateAllCustomInput = document.getElementById('custom-rotate-all-input') as HTMLInputElement;
|
||||
const rotateAllDecrementBtn = document.getElementById('rotate-all-decrement-btn');
|
||||
const rotateAllIncrementBtn = document.getElementById('rotate-all-increment-btn');
|
||||
|
||||
const rotateAllCustomBtn = document.getElementById(
|
||||
'rotate-all-custom-btn'
|
||||
);
|
||||
const rotateAllCustomInput = document.getElementById(
|
||||
'custom-rotate-all-input'
|
||||
) as HTMLInputElement;
|
||||
const rotateAllDecrementBtn = document.getElementById(
|
||||
'rotate-all-decrement-btn'
|
||||
);
|
||||
const rotateAllIncrementBtn = document.getElementById(
|
||||
'rotate-all-increment-btn'
|
||||
);
|
||||
|
||||
rotateAllControls.classList.remove('hidden');
|
||||
createIcons({ icons });
|
||||
@@ -136,12 +159,14 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
const rotateAll = (angle: number) => {
|
||||
// Update rotation state for ALL pages (including unrendered ones)
|
||||
for (let i = 0; i < rotationState.length; i++) {
|
||||
rotationState[i] = (rotationState[i] + angle);
|
||||
rotationState[i] = rotationState[i] + angle;
|
||||
}
|
||||
|
||||
// Update DOM for currently rendered pages
|
||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
|
||||
const pageIndex = parseInt(
|
||||
(item as HTMLElement).dataset.pageIndex || '0'
|
||||
);
|
||||
const newRotation = rotationState[pageIndex];
|
||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||
|
||||
@@ -463,7 +488,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
|
||||
addBtn.onclick = () => {
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.className = 'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper';
|
||||
fieldWrapper.className =
|
||||
'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper';
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
@@ -505,7 +531,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
|
||||
// Setup quality sliders for image conversion tools
|
||||
if (toolId === 'pdf-to-jpg') {
|
||||
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'jpg-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -517,7 +545,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-png') {
|
||||
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'png-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('png-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -529,7 +559,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-webp') {
|
||||
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'webp-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -624,16 +656,20 @@ async function handleMultiFileUpload(toolId) {
|
||||
const imageList = document.getElementById('image-list');
|
||||
|
||||
const renderedFiles = new Set(
|
||||
Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName)
|
||||
Array.from(imageList.querySelectorAll('li')).map(
|
||||
(li) => li.dataset.fileKey
|
||||
)
|
||||
);
|
||||
|
||||
state.files.forEach((file) => {
|
||||
state.files.forEach((file, index) => {
|
||||
if (!file) {
|
||||
console.error('Invalid file encountered in state.files');
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderedFiles.has(file.name)) {
|
||||
const fileKey = makeUniqueFileKey(index, file.name);
|
||||
|
||||
if (renderedFiles.has(fileKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -645,10 +681,12 @@ async function handleMultiFileUpload(toolId) {
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'relative group cursor-move';
|
||||
li.dataset.fileName = file.name;
|
||||
li.dataset.fileKey = fileKey;
|
||||
li.dataset.fileIndex = String(index);
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
|
||||
wrapper.className =
|
||||
'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
@@ -665,11 +703,14 @@ async function handleMultiFileUpload(toolId) {
|
||||
});
|
||||
|
||||
const syncStateWithDOM = () => {
|
||||
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName);
|
||||
state.files.sort((a, b) => {
|
||||
const aIndex = domOrder.indexOf(a.name);
|
||||
const bIndex = domOrder.indexOf(b.name);
|
||||
return aIndex - bIndex;
|
||||
const domOrder = Array.from(imageList.querySelectorAll('li')).map((li) =>
|
||||
Number(li.dataset.fileIndex)
|
||||
);
|
||||
const reordered = domOrder.map((i) => state.files[i]);
|
||||
state.files.length = 0;
|
||||
reordered.forEach((f) => state.files.push(f));
|
||||
Array.from(imageList.querySelectorAll('li')).forEach((li, i) => {
|
||||
(li as HTMLElement).dataset.fileIndex = String(i);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -678,7 +719,7 @@ async function handleMultiFileUpload(toolId) {
|
||||
animation: 150,
|
||||
onEnd: () => {
|
||||
syncStateWithDOM();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -687,10 +728,13 @@ async function handleMultiFileUpload(toolId) {
|
||||
const opts = document.getElementById('image-to-pdf-options');
|
||||
if (opts && opts.classList.contains('hidden')) {
|
||||
opts.classList.remove('hidden');
|
||||
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
|
||||
const slider = document.getElementById(
|
||||
'image-pdf-quality'
|
||||
) as HTMLInputElement;
|
||||
const value = document.getElementById('image-pdf-quality-value');
|
||||
if (slider && value) {
|
||||
const update = () => (value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
|
||||
const update = () =>
|
||||
(value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
|
||||
slider.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
@@ -698,7 +742,9 @@ async function handleMultiFileUpload(toolId) {
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-jpg') {
|
||||
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'jpg-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -710,7 +756,9 @@ async function handleMultiFileUpload(toolId) {
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-png') {
|
||||
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'png-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('png-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -722,7 +770,9 @@ async function handleMultiFileUpload(toolId) {
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-webp') {
|
||||
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const qualitySlider = document.getElementById(
|
||||
'webp-quality'
|
||||
) as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
@@ -750,11 +800,23 @@ export function setupFileInputHandler(toolId) {
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
if (toolId === 'image-to-pdf') {
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'];
|
||||
const validFiles = newFiles.filter(file => validTypes.includes(file.type));
|
||||
const validTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
];
|
||||
const validFiles = newFiles.filter((file) =>
|
||||
validTypes.includes(file.type)
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped because they are not supported images.');
|
||||
showAlert(
|
||||
'Invalid Files',
|
||||
'Some files were skipped because they are not supported images.'
|
||||
);
|
||||
}
|
||||
|
||||
newFiles = validFiles;
|
||||
@@ -780,7 +842,12 @@ export function setupFileInputHandler(toolId) {
|
||||
}
|
||||
|
||||
if (isMultiFileTool) {
|
||||
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments' || toolId === 'flatten') {
|
||||
if (
|
||||
toolId === 'txt-to-pdf' ||
|
||||
toolId === 'compress' ||
|
||||
toolId === 'extract-attachments' ||
|
||||
toolId === 'flatten'
|
||||
) {
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
@@ -839,7 +906,7 @@ export function setupFileInputHandler(toolId) {
|
||||
const clearBtn = document.getElementById('clear-files-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
activeImageUrls.forEach(url => URL.revokeObjectURL(url));
|
||||
activeImageUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||
activeImageUrls.clear();
|
||||
|
||||
state.files = [];
|
||||
|
||||
@@ -4,6 +4,7 @@ import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
||||
import {
|
||||
showWasmRequiredDialog,
|
||||
WasmProvider,
|
||||
@@ -72,19 +73,21 @@ async function updateUI() {
|
||||
fileList.innerHTML = '';
|
||||
|
||||
try {
|
||||
for (const file of pageState.files) {
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
const fileKey = makeUniqueFileKey(i, file.name);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfBytes.set(file.name, arrayBuffer);
|
||||
pageState.pdfBytes.set(fileKey, arrayBuffer);
|
||||
|
||||
const bytesForPdfJs = arrayBuffer.slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
pageState.pdfDocs.set(file.name, pdfjsDoc);
|
||||
pageState.pdfDocs.set(fileKey, pdfjsDoc);
|
||||
const pageCount = pdfjsDoc.numPages;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className =
|
||||
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||
li.dataset.fileName = file.name;
|
||||
li.dataset.fileName = fileKey;
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { formatBytes, downloadFile } from '../utils/helpers.js';
|
||||
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const embedPdfWasmUrl = new URL(
|
||||
'embedpdf-snippet/dist/pdfium.wasm',
|
||||
@@ -143,10 +144,10 @@ async function handleFiles(files: FileList) {
|
||||
|
||||
docManagerPlugin.onDocumentOpened((data: any) => {
|
||||
const docId = data?.id;
|
||||
const docName = data?.name;
|
||||
const docKey = data?.name;
|
||||
if (!docId) return;
|
||||
const pendingEntry = fileDisplayArea.querySelector(
|
||||
`[data-pending-name="${CSS.escape(docName)}"]`
|
||||
`[data-pending-name="${CSS.escape(docKey)}"]`
|
||||
) as HTMLElement;
|
||||
if (pendingEntry) {
|
||||
pendingEntry.removeAttribute('data-pending-name');
|
||||
@@ -166,7 +167,7 @@ async function handleFiles(files: FileList) {
|
||||
|
||||
docManagerPlugin.openDocumentBuffer({
|
||||
buffer: firstBuffer,
|
||||
name: firstFile.name,
|
||||
name: makeUniqueFileKey(0, firstFile.name),
|
||||
autoActivate: true,
|
||||
});
|
||||
|
||||
@@ -174,7 +175,7 @@ async function handleFiles(files: FileList) {
|
||||
const buffer = await pdfFiles[i].arrayBuffer();
|
||||
docManagerPlugin.openDocumentBuffer({
|
||||
buffer,
|
||||
name: pdfFiles[i].name,
|
||||
name: makeUniqueFileKey(i, pdfFiles[i].name),
|
||||
autoActivate: false,
|
||||
});
|
||||
}
|
||||
@@ -215,11 +216,11 @@ async function handleFiles(files: FileList) {
|
||||
} else {
|
||||
addFileEntries(fileDisplayArea, pdfFiles);
|
||||
|
||||
for (const file of pdfFiles) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
const buffer = await pdfFiles[i].arrayBuffer();
|
||||
docManagerPlugin.openDocumentBuffer({
|
||||
buffer,
|
||||
name: file.name,
|
||||
name: makeUniqueFileKey(i, pdfFiles[i].name),
|
||||
autoActivate: true,
|
||||
});
|
||||
}
|
||||
@@ -233,11 +234,12 @@ async function handleFiles(files: FileList) {
|
||||
}
|
||||
|
||||
function addFileEntries(fileDisplayArea: HTMLElement, files: File[]) {
|
||||
for (const file of files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
fileDiv.setAttribute('data-pending-name', file.name);
|
||||
fileDiv.setAttribute('data-pending-name', makeUniqueFileKey(i, file.name));
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import { flattenAnnotations } from '../utils/flatten-annotations.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { FlattenPdfState } from '@/types';
|
||||
|
||||
const pageState: FlattenPdfState = {
|
||||
@@ -138,7 +139,7 @@ async function flattenPdf() {
|
||||
|
||||
const newPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`flattened_${file.name}`
|
||||
);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
@@ -147,6 +148,7 @@ async function flattenPdf() {
|
||||
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
@@ -178,7 +180,11 @@ async function flattenPdf() {
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`flattened_${file.name}`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, flattenedBytes);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
interface FontToOutlineState {
|
||||
files: File[];
|
||||
@@ -128,6 +129,7 @@ async function processFiles() {
|
||||
if (loaderText) loaderText.textContent = 'Processing multiple PDFs...';
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
@@ -139,7 +141,11 @@ async function processFiles() {
|
||||
const resultBlob = await convertFileToOutlines(file, () => {});
|
||||
const arrayBuffer = await resultBlob.arrayBuffer();
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
zip.file(`${baseName}_outlined.pdf`, arrayBuffer);
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}_outlined.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, arrayBuffer);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
|
||||
@@ -1,237 +1,257 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { LinearizePdfState } from '@/types';
|
||||
|
||||
const pageState: LinearizePdfState = {
|
||||
files: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.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';
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.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 flex-col overflow-hidden';
|
||||
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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
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 = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
}
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files.push(...pdfFiles);
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(
|
||||
(f) =>
|
||||
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files.push(...pdfFiles);
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function linearizePdf() {
|
||||
const pdfFiles = pageState.files.filter(
|
||||
(file: File) => file.type === 'application/pdf'
|
||||
);
|
||||
if (!pdfFiles || pdfFiles.length === 0) {
|
||||
showAlert('No PDF Files', 'Please upload at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
const pdfFiles = pageState.files.filter(
|
||||
(file: File) => file.type === 'application/pdf'
|
||||
);
|
||||
if (!pdfFiles || pdfFiles.length === 0) {
|
||||
showAlert('No PDF Files', 'Please upload at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText)
|
||||
loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
|
||||
|
||||
const zip = new JSZip();
|
||||
let qpdf: any;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
let qpdf: any;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
const file = pdfFiles[i];
|
||||
const inputPath = `/input_${i}.pdf`;
|
||||
const outputPath = `/output_${i}.pdf`;
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
const file = pdfFiles[i];
|
||||
const inputPath = `/input_${i}.pdf`;
|
||||
const outputPath = `/output_${i}.pdf`;
|
||||
|
||||
if (loaderText) loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
|
||||
|
||||
try {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
try {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [inputPath, '--linearize', outputPath];
|
||||
const args = [inputPath, '--linearize', outputPath];
|
||||
|
||||
qpdf.callMain(args);
|
||||
qpdf.callMain(args);
|
||||
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
console.error(
|
||||
`Linearization resulted in an empty file for ${file.name}.`
|
||||
);
|
||||
throw new Error(`Processing failed for ${file.name}.`);
|
||||
}
|
||||
|
||||
zip.file(`linearized-${file.name}`, outputFile, { binary: true });
|
||||
successCount++;
|
||||
} catch (fileError: any) {
|
||||
errorCount++;
|
||||
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
if (qpdf.FS.analyzePath(inputPath).exists) {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
}
|
||||
if (qpdf.FS.analyzePath(outputPath).exists) {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
`Failed to cleanup WASM FS for ${file.name}:`,
|
||||
cleanupError
|
||||
);
|
||||
}
|
||||
}
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
console.error(
|
||||
`Linearization resulted in an empty file for ${file.name}.`
|
||||
);
|
||||
throw new Error(`Processing failed for ${file.name}.`);
|
||||
}
|
||||
|
||||
if (successCount === 0) {
|
||||
throw new Error('No PDF files could be linearized.');
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'linearized-pdfs.zip');
|
||||
|
||||
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
|
||||
if (errorCount > 0) {
|
||||
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||
}
|
||||
showAlert('Processing Complete', alertMessage, 'success', () => { resetState(); });
|
||||
} catch (error: any) {
|
||||
console.error('Linearization process error:', error);
|
||||
showAlert(
|
||||
'Linearization Failed',
|
||||
`An error occurred: ${error.message || 'Unknown error'}.`
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`linearized-${file.name}`,
|
||||
usedNames
|
||||
);
|
||||
} finally {
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
zip.file(zipEntryName, outputFile, { binary: true });
|
||||
successCount++;
|
||||
} catch (fileError: any) {
|
||||
errorCount++;
|
||||
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
if (qpdf.FS.analyzePath(inputPath).exists) {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
}
|
||||
if (qpdf.FS.analyzePath(outputPath).exists) {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
`Failed to cleanup WASM FS for ${file.name}:`,
|
||||
cleanupError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount === 0) {
|
||||
throw new Error('No PDF files could be linearized.');
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'linearized-pdfs.zip');
|
||||
|
||||
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
|
||||
if (errorCount > 0) {
|
||||
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||
}
|
||||
showAlert('Processing Complete', alertMessage, 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Linearization process error:', error);
|
||||
showAlert(
|
||||
'Linearization Failed',
|
||||
`An error occurred: ${error.message || 'Unknown error'}.`
|
||||
);
|
||||
} finally {
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', linearizePdf);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', linearizePdf);
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -131,8 +131,9 @@ async function renderPageMergeThumbnails() {
|
||||
cleanupLazyRendering();
|
||||
|
||||
let totalPages = 0;
|
||||
for (const file of state.files) {
|
||||
const doc = mergeState.pdfDocs[file.name];
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const fileKey = `${i}_${state.files[i].name}`;
|
||||
const doc = mergeState.pdfDocs[fileKey];
|
||||
if (doc) totalPages += doc.numPages;
|
||||
}
|
||||
|
||||
@@ -143,12 +144,13 @@ async function renderPageMergeThumbnails() {
|
||||
const createWrapper = (
|
||||
canvas: HTMLCanvasElement,
|
||||
pageNumber: number,
|
||||
fileName?: string
|
||||
fileKey: string,
|
||||
displayName: string
|
||||
) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||
wrapper.dataset.fileName = fileName || '';
|
||||
wrapper.dataset.fileName = fileKey;
|
||||
wrapper.dataset.pageIndex = (pageNumber - 1).toString();
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
@@ -168,29 +170,29 @@ async function renderPageMergeThumbnails() {
|
||||
const fileNamePara = document.createElement('p');
|
||||
fileNamePara.className =
|
||||
'text-xs text-gray-400 truncate w-full text-center';
|
||||
const fullTitle = fileName
|
||||
? `${fileName} (page ${pageNumber})`
|
||||
const fullTitle = displayName
|
||||
? `${displayName} (page ${pageNumber})`
|
||||
: `Page ${pageNumber}`;
|
||||
fileNamePara.title = fullTitle;
|
||||
fileNamePara.textContent = fileName
|
||||
? `${fileName.substring(0, 10)}... (p${pageNumber})`
|
||||
fileNamePara.textContent = displayName
|
||||
? `${displayName.substring(0, 10)}... (p${pageNumber})`
|
||||
: `Page ${pageNumber}`;
|
||||
|
||||
wrapper.append(imgContainer, fileNamePara);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
// Render pages from all files progressively
|
||||
for (const file of state.files) {
|
||||
const pdfjsDoc = mergeState.pdfDocs[file.name];
|
||||
for (let idx = 0; idx < state.files.length; idx++) {
|
||||
const file = state.files[idx];
|
||||
const fileKey = `${idx}_${file.name}`;
|
||||
const pdfjsDoc = mergeState.pdfDocs[fileKey];
|
||||
if (!pdfjsDoc) continue;
|
||||
|
||||
// Create a wrapper function that includes the file name
|
||||
const createWrapperWithFileName = (
|
||||
canvas: HTMLCanvasElement,
|
||||
pageNumber: number
|
||||
) => {
|
||||
return createWrapper(canvas, pageNumber, file.name);
|
||||
return createWrapper(canvas, pageNumber, fileKey, file.name);
|
||||
};
|
||||
|
||||
// Render pages progressively with lazy loading
|
||||
@@ -299,32 +301,27 @@ export async function merge() {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
|
||||
const sortedFiles = Array.from(fileList.children)
|
||||
.map((li) => {
|
||||
return state.files.find(
|
||||
(f) => f.name === (li as HTMLElement).dataset.fileName
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
const sortedFileKeys = Array.from(fileList.children)
|
||||
.map((li) => (li as HTMLElement).dataset.fileName)
|
||||
.filter((key): key is string => !!key);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
if (!file) continue;
|
||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
for (const fileKey of sortedFileKeys) {
|
||||
const safeFileName = fileKey.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const rangeInput = document.getElementById(
|
||||
`range-${safeFileName}`
|
||||
) as HTMLInputElement;
|
||||
|
||||
uniqueFileNames.add(file.name);
|
||||
uniqueFileNames.add(fileKey);
|
||||
|
||||
if (rangeInput && rangeInput.value.trim()) {
|
||||
jobs.push({
|
||||
fileName: file.name,
|
||||
fileName: fileKey,
|
||||
rangeType: 'specific',
|
||||
rangeString: rangeInput.value.trim(),
|
||||
});
|
||||
} else {
|
||||
jobs.push({
|
||||
fileName: file.name,
|
||||
fileName: fileKey,
|
||||
rangeType: 'all',
|
||||
});
|
||||
}
|
||||
@@ -456,13 +453,15 @@ export async function refreshMergeUI() {
|
||||
mergeState.pdfDocs = {};
|
||||
mergeState.pdfBytes = {};
|
||||
|
||||
for (const file of state.files) {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
const fileKey = `${i}_${file.name}`;
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||
mergeState.pdfBytes[fileKey] = pdfBytes as ArrayBuffer;
|
||||
|
||||
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
mergeState.pdfDocs[file.name] = pdfjsDoc;
|
||||
mergeState.pdfDocs[fileKey] = pdfjsDoc;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
@@ -483,14 +482,15 @@ export async function refreshMergeUI() {
|
||||
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach((f, index) => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const fileKey = `${index}_${f.name}`;
|
||||
const doc = mergeState.pdfDocs[fileKey];
|
||||
const pageCount = doc ? doc.numPages : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const safeFileName = fileKey.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className =
|
||||
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||
li.dataset.fileName = f.name;
|
||||
li.dataset.fileName = fileKey;
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.className = 'flex items-center justify-between';
|
||||
|
||||
@@ -1,188 +1,215 @@
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.pages'];
|
||||
const FILETYPE_NAME = 'Pages';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.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',
|
||||
`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();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
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(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, 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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.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();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -124,6 +125,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader('Converting multiple PDFs...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
@@ -134,7 +136,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
const arrayBuffer = await docxBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.docx`, arrayBuffer);
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.docx`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, arrayBuffer);
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
formatBytes,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||
import {
|
||||
@@ -144,10 +145,12 @@ worker.onmessage = async (e: MessageEvent) => {
|
||||
showStatus('Creating ZIP file...', 'info');
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
jsonFiles.forEach(({ name, data }) => {
|
||||
const jsonName = name.replace(/\.pdf$/i, '.json');
|
||||
const uint8Array = new Uint8Array(data);
|
||||
zip.file(jsonName, uint8Array);
|
||||
const zipEntryName = deduplicateFileName(jsonName, usedNames);
|
||||
zip.file(zipEntryName, uint8Array);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -130,6 +131,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader('Converting multiple PDFs...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
@@ -139,7 +141,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
zip.file(`${baseName}.md`, markdown);
|
||||
const zipEntryName = deduplicateFileName(`${baseName}.md`, usedNames);
|
||||
zip.file(zipEntryName, markdown);
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -169,6 +170,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader('Converting multiple PDFs to PDF/A...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
@@ -182,7 +184,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
const blobBuffer = await convertedBlob.arrayBuffer();
|
||||
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}_pdfa.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, blobBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
@@ -4,6 +4,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: any = null;
|
||||
@@ -196,6 +197,7 @@ async function extractText() {
|
||||
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
@@ -206,7 +208,8 @@ async function extractText() {
|
||||
const fullText = await mupdf.pdfToText(file);
|
||||
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
zip.file(`${baseName}.txt`, fullText);
|
||||
const zipEntryName = deduplicateFileName(`${baseName}.txt`, usedNames);
|
||||
zip.file(zipEntryName, fullText);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
@@ -2,159 +2,173 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
interface PdfToZipState {
|
||||
files: File[];
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const pageState: PdfToZipState = {
|
||||
files: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.files.forEach(function (file, index) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (pageState.files.length > 0) {
|
||||
pageState.files.forEach(function (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 flex-col overflow-hidden';
|
||||
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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = function () {
|
||||
pageState.files = pageState.files.filter(function (_, i) { return i !== index; });
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
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 = function () {
|
||||
pageState.files = pageState.files.filter(function (_, i) {
|
||||
return i !== index;
|
||||
});
|
||||
updateUI();
|
||||
};
|
||||
|
||||
createIcons({ icons });
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function createZipArchive() {
|
||||
if (pageState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select PDF files to create a ZIP archive.');
|
||||
return;
|
||||
if (pageState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select PDF files to create a ZIP archive.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
showLoader(`Adding ${file.name} (${i + 1}/${pageState.files.length})...`);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const zipEntryName = deduplicateFileName(file.name, usedNames);
|
||||
zip.file(zipEntryName, arrayBuffer);
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
showLoader('Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
downloadFile(zipBlob, 'pdfs_archive.zip');
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
showLoader(`Adding ${file.name} (${i + 1}/${pageState.files.length})...`);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
zip.file(file.name, arrayBuffer);
|
||||
}
|
||||
|
||||
showLoader('Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'pdfs_archive.zip');
|
||||
|
||||
showAlert('Success', 'ZIP archive created successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not create ZIP archive.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
showAlert(
|
||||
'Success',
|
||||
'ZIP archive created successfully!',
|
||||
'success',
|
||||
function () {
|
||||
resetState();
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not create ZIP archive.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files = [...pageState.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return (
|
||||
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
pageState.files = [...pageState.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', createZipArchive);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', createZipArchive);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,218 +1,237 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
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');
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
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';
|
||||
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 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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PowerPoint file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.(ppt|pptx|odp)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Processing...');
|
||||
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(/\.(ppt|pptx|odp)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'powerpoint-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PowerPoint file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pptFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return name.endsWith('.ppt') || name.endsWith('.pptx') || name.endsWith('.odp');
|
||||
});
|
||||
if (pptFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
pptFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
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 = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PowerPoint file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName =
|
||||
originalFile.name.replace(/\.(ppt|pptx|odp)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
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(/\.(ppt|pptx|odp)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'powerpoint-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PowerPoint file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pptFiles = Array.from(files).filter((f) => {
|
||||
const name = f.name.toLowerCase();
|
||||
return (
|
||||
name.endsWith('.ppt') ||
|
||||
name.endsWith('.pptx') ||
|
||||
name.endsWith('.odp')
|
||||
);
|
||||
});
|
||||
if (pptFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
pptFiles.forEach((f) => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -132,6 +131,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Multiple files - create ZIP
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (const file of state.files) {
|
||||
try {
|
||||
@@ -142,7 +142,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||
zip.file(outName, jsonContent);
|
||||
const zipEntryName = deduplicateFileName(outName, usedNames);
|
||||
zip.file(zipEntryName, jsonContent);
|
||||
|
||||
completed++;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,141 +2,185 @@ import { showLoader, hideLoader, showAlert } from '../ui.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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.pub'];
|
||||
const FILETYPE_NAME = 'PUB';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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',
|
||||
`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();
|
||||
const usedNames = new Set<string>();
|
||||
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(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, 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();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -151,6 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Multiple files - create ZIP
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (const file of state.files) {
|
||||
try {
|
||||
@@ -167,7 +169,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const outName =
|
||||
file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||
zip.file(outName, rasterizedBlob);
|
||||
const zipEntryName = deduplicateFileName(outName, usedNames);
|
||||
zip.file(zipEntryName, rasterizedBlob);
|
||||
|
||||
completed++;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,128 +1,135 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/repaired_form.pdf';
|
||||
let qpdf: any;
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/repaired_form.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [inputPath, '--decrypt', outputPath];
|
||||
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [inputPath, '--decrypt', outputPath];
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (e) {
|
||||
console.warn(`QPDF execution warning for ${file.name}:`, e);
|
||||
}
|
||||
|
||||
let repairedData: Uint8Array | null = null;
|
||||
try {
|
||||
repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
} catch (e) {
|
||||
console.warn(`Failed to read output for ${file.name}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Cleanup error:', cleanupError);
|
||||
}
|
||||
|
||||
return repairedData;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error repairing ${file.name}:`, error);
|
||||
return null;
|
||||
qpdf.callMain(args);
|
||||
} catch (e) {
|
||||
console.warn(`QPDF execution warning for ${file.name}:`, e);
|
||||
}
|
||||
|
||||
let repairedData: Uint8Array | null = null;
|
||||
try {
|
||||
repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
} catch (e) {
|
||||
console.warn(`Failed to read output for ${file.name}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Cleanup error:', cleanupError);
|
||||
}
|
||||
|
||||
return repairedData;
|
||||
} catch (error) {
|
||||
console.error(`Error repairing ${file.name}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function repairPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
}
|
||||
|
||||
const successfulRepairs: { name: string; data: Uint8Array }[] = [];
|
||||
const failedRepairs: string[] = [];
|
||||
|
||||
try {
|
||||
showLoader('Initializing repair engine...');
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
|
||||
|
||||
const repairedData = await repairPdfFile(file);
|
||||
|
||||
if (repairedData && repairedData.length > 0) {
|
||||
successfulRepairs.push({
|
||||
name: `repaired-${file.name}`,
|
||||
data: repairedData,
|
||||
});
|
||||
} else {
|
||||
failedRepairs.push(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRepairs: { name: string; data: Uint8Array }[] = [];
|
||||
const failedRepairs: string[] = [];
|
||||
hideLoader();
|
||||
|
||||
try {
|
||||
showLoader('Initializing repair engine...');
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
|
||||
|
||||
const repairedData = await repairPdfFile(file);
|
||||
|
||||
if (repairedData && repairedData.length > 0) {
|
||||
successfulRepairs.push({
|
||||
name: `repaired-${file.name}`,
|
||||
data: repairedData,
|
||||
});
|
||||
} else {
|
||||
failedRepairs.push(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (successfulRepairs.length === 0) {
|
||||
showAlert('Repair Failed', 'Unable to repair any of the uploaded PDF files.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedRepairs.length > 0) {
|
||||
const failedList = failedRepairs.join(', ');
|
||||
showAlert(
|
||||
'Partial Success',
|
||||
`Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
|
||||
);
|
||||
}
|
||||
|
||||
if (successfulRepairs.length === 1) {
|
||||
const file = successfulRepairs[0];
|
||||
const blob = new Blob([file.data as any], { type: 'application/pdf' });
|
||||
downloadFile(blob, file.name);
|
||||
} else {
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zip = new JSZip();
|
||||
successfulRepairs.forEach((file) => {
|
||||
zip.file(file.name, file.data);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'repaired_pdfs.zip');
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
if (failedRepairs.length === 0) {
|
||||
showAlert('Success', 'All files repaired successfully!');
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Critical error during repair:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'An unexpected error occurred during the repair process.');
|
||||
if (successfulRepairs.length === 0) {
|
||||
showAlert(
|
||||
'Repair Failed',
|
||||
'Unable to repair any of the uploaded PDF files.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedRepairs.length > 0) {
|
||||
const failedList = failedRepairs.join(', ');
|
||||
showAlert(
|
||||
'Partial Success',
|
||||
`Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
|
||||
);
|
||||
}
|
||||
|
||||
if (successfulRepairs.length === 1) {
|
||||
const file = successfulRepairs[0];
|
||||
const blob = new Blob([file.data as any], { type: 'application/pdf' });
|
||||
downloadFile(blob, file.name);
|
||||
} else {
|
||||
showLoader('Creating ZIP archive...');
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
successfulRepairs.forEach((file) => {
|
||||
const zipEntryName = deduplicateFileName(file.name, usedNames);
|
||||
zip.file(zipEntryName, file.data);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'repaired_pdfs.zip');
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
if (failedRepairs.length === 0) {
|
||||
showAlert('Success', 'All files repaired successfully!');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Critical error during repair:', error);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
'An unexpected error occurred during the repair process.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,205 +3,232 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
interface ReverseState {
|
||||
files: File[];
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const reverseState: ReverseState = {
|
||||
files: [],
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
reverseState.files = [];
|
||||
reverseState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (reverseState.files.length > 0) {
|
||||
reverseState.files.forEach(function (file, index) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
if (reverseState.files.length > 0) {
|
||||
reverseState.files.forEach(function (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 flex-col overflow-hidden';
|
||||
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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = function () {
|
||||
reverseState.files = reverseState.files.filter(function (_, i) { return i !== index; });
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
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 = function () {
|
||||
reverseState.files = reverseState.files.filter(function (_, i) {
|
||||
return i !== index;
|
||||
});
|
||||
updateUI();
|
||||
};
|
||||
|
||||
createIcons({ icons });
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function reversePages() {
|
||||
if (reverseState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
if (reverseState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Reversing page order...');
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let j = 0; j < reverseState.files.length; j++) {
|
||||
const file = reverseState.files[j];
|
||||
showLoader(
|
||||
`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`
|
||||
);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
{ length: pageCount },
|
||||
function (_, i) {
|
||||
return pageCount - 1 - i;
|
||||
}
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||
copiedPages.forEach(function (page) {
|
||||
newPdf.addPage(page);
|
||||
});
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||
const fileName = `${originalName}_reversed.pdf`;
|
||||
const zipEntryName = deduplicateFileName(fileName, usedNames);
|
||||
zip.file(zipEntryName, newPdfBytes);
|
||||
}
|
||||
|
||||
showLoader('Reversing page order...');
|
||||
if (reverseState.files.length === 1) {
|
||||
// Single file: download directly
|
||||
const file = reverseState.files[0];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let j = 0; j < reverseState.files.length; j++) {
|
||||
const file = reverseState.files[j];
|
||||
showLoader(`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`);
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
{ length: pageCount },
|
||||
function (_, i) { return pageCount - 1 - i; }
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||
copiedPages.forEach(function (page) { newPdf.addPage(page); });
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||
const fileName = `${originalName}_reversed.pdf`;
|
||||
zip.file(fileName, newPdfBytes);
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
{ length: pageCount },
|
||||
function (_, i) {
|
||||
return pageCount - 1 - i;
|
||||
}
|
||||
);
|
||||
|
||||
if (reverseState.files.length === 1) {
|
||||
// Single file: download directly
|
||||
const file = reverseState.files[0];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||
copiedPages.forEach(function (page) {
|
||||
newPdf.addPage(page);
|
||||
});
|
||||
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
{ length: pageCount },
|
||||
function (_, i) { return pageCount - 1 - i; }
|
||||
);
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||
copiedPages.forEach(function (page) { newPdf.addPage(page); });
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_reversed.pdf`
|
||||
);
|
||||
} else {
|
||||
// Multiple files: download as ZIP
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||
}
|
||||
|
||||
showAlert('Success', 'Pages have been reversed successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not reverse the PDF pages. Please check that your files are valid PDFs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_reversed.pdf`
|
||||
);
|
||||
} else {
|
||||
// Multiple files: download as ZIP
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||
}
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'Pages have been reversed successfully!',
|
||||
'success',
|
||||
function () {
|
||||
resetState();
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not reverse the PDF pages. Please check that your files are valid PDFs.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
reverseState.files = [...reverseState.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return (
|
||||
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||
);
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
reverseState.files = [...reverseState.files, ...pdfFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', reversePages);
|
||||
}
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', reversePages);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,215 +1,234 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
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');
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
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';
|
||||
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 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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one RTF file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.rtf$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple RTF files to PDF...');
|
||||
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(/\.rtf$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'rtf-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} RTF file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const rtfFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.rtf') || f.type === 'text/rtf' || f.type === 'application/rtf');
|
||||
if (rtfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
rtfFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
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 = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one RTF file.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.rtf$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple RTF files to PDF...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
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(/\.rtf$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
downloadFile(zipBlob, 'rtf-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} RTF file(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const rtfFiles = Array.from(files).filter(
|
||||
(f) =>
|
||||
f.name.toLowerCase().endsWith('.rtf') ||
|
||||
f.type === 'text/rtf' ||
|
||||
f.type === 'application/rtf'
|
||||
);
|
||||
if (rtfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
rtfFiles.forEach((f) => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
|
||||
@@ -2,141 +2,185 @@ import { showLoader, hideLoader, showAlert } from '../ui.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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.vsd', '.vsdx'];
|
||||
const FILETYPE_NAME = 'VSD';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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',
|
||||
`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();
|
||||
const usedNames = new Set<string>();
|
||||
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(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, 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();
|
||||
});
|
||||
|
||||
@@ -1,236 +1,269 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
formatBytes,
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [];
|
||||
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');
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
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';
|
||||
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 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 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);
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
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 = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
console.log('[Word2PDF] Starting conversion...');
|
||||
console.log('[Word2PDF] Number of files:', state.files.length);
|
||||
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one Word document.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
console.log('[Word2PDF] Got converter instance');
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
console.log('[Word2PDF] Initializing LibreOffice...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
console.log('[Word2PDF] Init progress:', progress.percent + '%', progress.message);
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
console.log('[Word2PDF] LibreOffice initialized successfully!');
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
console.log('[Word2PDF] Converting single file:', originalFile.name);
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
console.log('[Word2PDF] Conversion complete! PDF size:', pdfBlob.size);
|
||||
|
||||
const fileName = originalFile.name.replace(/\.(doc|docx|odt|rtf)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
console.log('[Word2PDF] File downloaded:', fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
console.log('[Word2PDF] Converting multiple files:', state.files.length);
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
console.log(`[Word2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name);
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
console.log(`[Word2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size);
|
||||
|
||||
const baseName = file.name.replace(/\.(doc|docx|odt|rtf)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
}
|
||||
|
||||
console.log('[Word2PDF] Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
console.log('[Word2PDF] ZIP size:', zipBlob.size);
|
||||
|
||||
downloadFile(zipBlob, 'word-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} Word document(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[Word2PDF] ERROR:', e);
|
||||
console.error('[Word2PDF] Error stack:', e.stack);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const wordFiles = Array.from(files).filter(f => {
|
||||
const name = f.name.toLowerCase();
|
||||
return name.endsWith('.doc') || name.endsWith('.docx') || name.endsWith('.odt') || name.endsWith('.rtf');
|
||||
});
|
||||
if (wordFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
wordFiles.forEach(f => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
// Initialize UI state (ensures button is hidden when no files)
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
console.log('[Word2PDF] Starting conversion...');
|
||||
console.log('[Word2PDF] Number of files:', state.files.length);
|
||||
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one Word document.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const converter = getLibreOfficeConverter();
|
||||
console.log('[Word2PDF] Got converter instance');
|
||||
|
||||
// Initialize LibreOffice if not already done
|
||||
console.log('[Word2PDF] Initializing LibreOffice...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
console.log(
|
||||
'[Word2PDF] Init progress:',
|
||||
progress.percent + '%',
|
||||
progress.message
|
||||
);
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
console.log('[Word2PDF] LibreOffice initialized successfully!');
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
console.log('[Word2PDF] Converting single file:', originalFile.name);
|
||||
|
||||
showLoader('Processing...');
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(originalFile);
|
||||
console.log('[Word2PDF] Conversion complete! PDF size:', pdfBlob.size);
|
||||
|
||||
const fileName =
|
||||
originalFile.name.replace(/\.(doc|docx|odt|rtf)$/i, '') + '.pdf';
|
||||
|
||||
downloadFile(pdfBlob, fileName);
|
||||
console.log('[Word2PDF] File downloaded:', fileName);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'[Word2PDF] Converting multiple files:',
|
||||
state.files.length
|
||||
);
|
||||
showLoader('Processing...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
console.log(
|
||||
`[Word2PDF] Converting file ${i + 1}/${state.files.length}:`,
|
||||
file.name
|
||||
);
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
console.log(
|
||||
`[Word2PDF] Converted ${file.name}, PDF size:`,
|
||||
pdfBlob.size
|
||||
);
|
||||
|
||||
const baseName = file.name.replace(/\.(doc|docx|odt|rtf)$/i, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, pdfBuffer);
|
||||
}
|
||||
|
||||
console.log('[Word2PDF] Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
console.log('[Word2PDF] ZIP size:', zipBlob.size);
|
||||
|
||||
downloadFile(zipBlob, 'word-converted.zip');
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} Word document(s) to PDF.`,
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[Word2PDF] ERROR:', e);
|
||||
console.error('[Word2PDF] Error stack:', e.stack);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
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');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const wordFiles = Array.from(files).filter((f) => {
|
||||
const name = f.name.toLowerCase();
|
||||
return (
|
||||
name.endsWith('.doc') ||
|
||||
name.endsWith('.docx') ||
|
||||
name.endsWith('.odt') ||
|
||||
name.endsWith('.rtf')
|
||||
);
|
||||
});
|
||||
if (wordFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
wordFiles.forEach((f) => dataTransfer.items.add(f));
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
// Initialize UI state (ensures button is hidden when no files)
|
||||
updateUI();
|
||||
});
|
||||
|
||||
@@ -1,188 +1,215 @@
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.wpd'];
|
||||
const FILETYPE_NAME = 'WPD';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.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',
|
||||
`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();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
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(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, 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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.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();
|
||||
});
|
||||
|
||||
@@ -1,188 +1,215 @@
|
||||
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 { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.wps'];
|
||||
const FILETYPE_NAME = 'WPS';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${e.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',
|
||||
`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();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
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(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, 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 (e: any) {
|
||||
hideLoader();
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e.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();
|
||||
});
|
||||
|
||||
@@ -1,181 +1,205 @@
|
||||
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 { convertXmlToPdf } from '../utils/xml-to-pdf.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.xml'];
|
||||
const FILETYPE_NAME = 'XML';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
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 fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
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');
|
||||
|
||||
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 updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
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 });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.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;
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||
try {
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(message, percent);
|
||||
},
|
||||
});
|
||||
|
||||
if (state.files.length > 0) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
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';
|
||||
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();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(
|
||||
`File ${i + 1}/${state.files.length}: ${message}`,
|
||||
percent
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
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 });
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, pdfBlob);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
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();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(message, percent);
|
||||
}
|
||||
});
|
||||
|
||||
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];
|
||||
const pdfBlob = await convertXmlToPdf(file, {
|
||||
onProgress: (percent, message) => {
|
||||
showLoader(`File ${i + 1}/${state.files.length}: ${message}`, percent);
|
||||
}
|
||||
});
|
||||
|
||||
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 (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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
const FILETYPE = 'xps';
|
||||
const EXTENSIONS = ['.xps', '.oxps'];
|
||||
@@ -114,6 +115,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader('Converting files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
@@ -126,7 +128,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||
const zipEntryName = deduplicateFileName(
|
||||
`${baseName}.pdf`,
|
||||
usedNames
|
||||
);
|
||||
zip.file(zipEntryName, pdfBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
28
src/js/utils/deduplicate-filename.ts
Normal file
28
src/js/utils/deduplicate-filename.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function deduplicateFileName(
|
||||
name: string,
|
||||
usedNames: Set<string>
|
||||
): string {
|
||||
if (!usedNames.has(name)) {
|
||||
usedNames.add(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
const dotIndex = name.lastIndexOf('.');
|
||||
const hasExtension = dotIndex > 0;
|
||||
const baseName = hasExtension ? name.slice(0, dotIndex) : name;
|
||||
const extension = hasExtension ? name.slice(dotIndex) : '';
|
||||
|
||||
let counter = 1;
|
||||
let candidate = `${baseName} (${counter})${extension}`;
|
||||
while (usedNames.has(candidate)) {
|
||||
counter++;
|
||||
candidate = `${baseName} (${counter})${extension}`;
|
||||
}
|
||||
|
||||
usedNames.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function makeUniqueFileKey(index: number, name: string): string {
|
||||
return `${index}_${name}`;
|
||||
}
|
||||
198
src/tests/deduplicate-filename.test.ts
Normal file
198
src/tests/deduplicate-filename.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
deduplicateFileName,
|
||||
makeUniqueFileKey,
|
||||
} from '@/js/utils/deduplicate-filename';
|
||||
|
||||
describe('deduplicateFileName', () => {
|
||||
it('should return the original name when no duplicates exist', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('report.pdf', usedNames)).toBe('report.pdf');
|
||||
});
|
||||
|
||||
it('should add the name to the Set after returning', () => {
|
||||
const usedNames = new Set<string>();
|
||||
deduplicateFileName('report.pdf', usedNames);
|
||||
expect(usedNames.has('report.pdf')).toBe(true);
|
||||
});
|
||||
|
||||
it('should append (1) for the first duplicate', () => {
|
||||
const usedNames = new Set<string>();
|
||||
deduplicateFileName('report.pdf', usedNames);
|
||||
expect(deduplicateFileName('report.pdf', usedNames)).toBe('report (1).pdf');
|
||||
});
|
||||
|
||||
it('should increment counter for multiple duplicates', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file.pdf');
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (1).pdf');
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (3).pdf');
|
||||
});
|
||||
|
||||
it('should track deduplicated names to avoid collisions with them', () => {
|
||||
const usedNames = new Set<string>();
|
||||
deduplicateFileName('file.pdf', usedNames);
|
||||
deduplicateFileName('file (1).pdf', usedNames);
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
|
||||
});
|
||||
|
||||
it('should handle files with no extension', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('README', usedNames)).toBe('README');
|
||||
expect(deduplicateFileName('README', usedNames)).toBe('README (1)');
|
||||
expect(deduplicateFileName('README', usedNames)).toBe('README (2)');
|
||||
});
|
||||
|
||||
it('should handle files with multiple dots in the name', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('my.report.2024.pdf', usedNames)).toBe(
|
||||
'my.report.2024.pdf'
|
||||
);
|
||||
expect(deduplicateFileName('my.report.2024.pdf', usedNames)).toBe(
|
||||
'my.report.2024 (1).pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle dotfiles (name starts with dot)', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore');
|
||||
expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore (1)');
|
||||
});
|
||||
|
||||
it('should handle dotfiles with extension', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('.env.local', usedNames)).toBe('.env.local');
|
||||
expect(deduplicateFileName('.env.local', usedNames)).toBe('.env (1).local');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('', usedNames)).toBe('');
|
||||
expect(deduplicateFileName('', usedNames)).toBe(' (1)');
|
||||
});
|
||||
|
||||
it('should handle extension-only name', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf');
|
||||
expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf (1)');
|
||||
});
|
||||
|
||||
it('should preserve the original extension exactly', () => {
|
||||
const usedNames = new Set<string>();
|
||||
deduplicateFileName('photo.JPEG', usedNames);
|
||||
const result = deduplicateFileName('photo.JPEG', usedNames);
|
||||
expect(result).toBe('photo (1).JPEG');
|
||||
expect(result.endsWith('.JPEG')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle very long filenames', () => {
|
||||
const usedNames = new Set<string>();
|
||||
const longName = 'a'.repeat(200) + '.pdf';
|
||||
expect(deduplicateFileName(longName, usedNames)).toBe(longName);
|
||||
expect(deduplicateFileName(longName, usedNames)).toBe(
|
||||
'a'.repeat(200) + ' (1).pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle names with special characters', () => {
|
||||
const usedNames = new Set<string>();
|
||||
const name = 'report (final) [v2].pdf';
|
||||
expect(deduplicateFileName(name, usedNames)).toBe(name);
|
||||
expect(deduplicateFileName(name, usedNames)).toBe(
|
||||
'report (final) [v2] (1).pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle names with unicode characters', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('レポート.pdf', usedNames)).toBe('レポート.pdf');
|
||||
expect(deduplicateFileName('レポート.pdf', usedNames)).toBe(
|
||||
'レポート (1).pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle names with spaces', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('my report.pdf', usedNames)).toBe(
|
||||
'my report.pdf'
|
||||
);
|
||||
expect(deduplicateFileName('my report.pdf', usedNames)).toBe(
|
||||
'my report (1).pdf'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not confuse different extensions as duplicates', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file.pdf');
|
||||
expect(deduplicateFileName('file.txt', usedNames)).toBe('file.txt');
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (1).pdf');
|
||||
expect(deduplicateFileName('file.txt', usedNames)).toBe('file (1).txt');
|
||||
});
|
||||
|
||||
it('should work with a fresh Set for each batch', () => {
|
||||
const batch1 = new Set<string>();
|
||||
deduplicateFileName('file.pdf', batch1);
|
||||
deduplicateFileName('file.pdf', batch1);
|
||||
|
||||
const batch2 = new Set<string>();
|
||||
expect(deduplicateFileName('file.pdf', batch2)).toBe('file.pdf');
|
||||
});
|
||||
|
||||
it('should handle many duplicates without infinite loop', () => {
|
||||
const usedNames = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
deduplicateFileName('test.pdf', usedNames);
|
||||
}
|
||||
expect(usedNames.size).toBe(100);
|
||||
expect(usedNames.has('test.pdf')).toBe(true);
|
||||
expect(usedNames.has('test (1).pdf')).toBe(true);
|
||||
expect(usedNames.has('test (99).pdf')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle pre-populated Set', () => {
|
||||
const usedNames = new Set<string>(['file.pdf', 'file (1).pdf']);
|
||||
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
|
||||
});
|
||||
|
||||
it('should handle name that is just a dot', () => {
|
||||
const usedNames = new Set<string>();
|
||||
expect(deduplicateFileName('.', usedNames)).toBe('.');
|
||||
expect(deduplicateFileName('.', usedNames)).toBe('. (1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeUniqueFileKey', () => {
|
||||
it('should combine index and name', () => {
|
||||
expect(makeUniqueFileKey(0, 'file.pdf')).toBe('0_file.pdf');
|
||||
expect(makeUniqueFileKey(1, 'file.pdf')).toBe('1_file.pdf');
|
||||
});
|
||||
|
||||
it('should produce different keys for same name at different indices', () => {
|
||||
const key1 = makeUniqueFileKey(0, 'report.pdf');
|
||||
const key2 = makeUniqueFileKey(1, 'report.pdf');
|
||||
expect(key1).not.toBe(key2);
|
||||
});
|
||||
|
||||
it('should produce same key for same index and name', () => {
|
||||
expect(makeUniqueFileKey(5, 'test.pdf')).toBe(
|
||||
makeUniqueFileKey(5, 'test.pdf')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty name', () => {
|
||||
expect(makeUniqueFileKey(0, '')).toBe('0_');
|
||||
});
|
||||
|
||||
it('should handle large indices', () => {
|
||||
expect(makeUniqueFileKey(9999, 'file.pdf')).toBe('9999_file.pdf');
|
||||
});
|
||||
|
||||
it('should handle names with underscores', () => {
|
||||
expect(makeUniqueFileKey(0, 'my_file.pdf')).toBe('0_my_file.pdf');
|
||||
});
|
||||
|
||||
it('should handle names with special characters', () => {
|
||||
expect(makeUniqueFileKey(0, 'file (1).pdf')).toBe('0_file (1).pdf');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user