From f88f872162a9cef90a8258298195e0b503f9df6b Mon Sep 17 00:00:00 2001 From: alam00000 Date: Tue, 24 Mar 2026 20:36:56 +0530 Subject: [PATCH] feat: implement filename deduplication utility and integrate across multiple file conversion modules --- src/js/handlers/fileHandler.ts | 141 ++++++-- src/js/logic/alternate-merge-page.ts | 11 +- src/js/logic/edit-pdf-page.ts | 20 +- src/js/logic/flatten-pdf-page.ts | 10 +- src/js/logic/font-to-outline-page.ts | 8 +- src/js/logic/linearize-pdf-page.ts | 372 ++++++++++--------- src/js/logic/merge-pdf-page.ts | 64 ++-- src/js/logic/pages-to-pdf-page.ts | 381 +++++++++++--------- src/js/logic/pdf-to-docx-page.ts | 8 +- src/js/logic/pdf-to-json.ts | 5 +- src/js/logic/pdf-to-markdown-page.ts | 5 +- src/js/logic/pdf-to-pdfa-page.ts | 8 +- src/js/logic/pdf-to-text-page.ts | 5 +- src/js/logic/pdf-to-zip-page.ts | 232 ++++++------ src/js/logic/powerpoint-to-pdf-page.ts | 407 +++++++++++---------- src/js/logic/prepare-pdf-for-ai-page.ts | 9 +- src/js/logic/pub-to-pdf-page.ts | 304 +++++++++------- src/js/logic/rasterize-pdf-page.ts | 5 +- src/js/logic/repair-pdf.ts | 227 ++++++------ src/js/logic/reverse-pages-page.ts | 329 +++++++++-------- src/js/logic/rtf-to-pdf-page.ts | 401 +++++++++++---------- src/js/logic/vsd-to-pdf-page.ts | 304 +++++++++------- src/js/logic/word-to-pdf-page.ts | 457 +++++++++++++----------- src/js/logic/wpd-to-pdf-page.ts | 381 +++++++++++--------- src/js/logic/wps-to-pdf-page.ts | 381 +++++++++++--------- src/js/logic/xml-to-pdf-page.ts | 338 ++++++++++-------- src/js/logic/xps-to-pdf-page.ts | 8 +- src/js/utils/deduplicate-filename.ts | 28 ++ src/tests/deduplicate-filename.test.ts | 198 ++++++++++ 29 files changed, 2860 insertions(+), 2187 deletions(-) create mode 100644 src/js/utils/deduplicate-filename.ts create mode 100644 src/tests/deduplicate-filename.test.ts diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 1622de9..6be0dce 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -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(); - 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 = []; diff --git a/src/js/logic/alternate-merge-page.ts b/src/js/logic/alternate-merge-page.ts index 6b32aed..9151789 100644 --- a/src/js/logic/alternate-merge-page.ts +++ b/src/js/logic/alternate-merge-page.ts @@ -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'; diff --git a/src/js/logic/edit-pdf-page.ts b/src/js/logic/edit-pdf-page.ts index d6a7e98..25041eb 100644 --- a/src/js/logic/edit-pdf-page.ts +++ b/src/js/logic/edit-pdf-page.ts @@ -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'; diff --git a/src/js/logic/flatten-pdf-page.ts b/src/js/logic/flatten-pdf-page.ts index 520c11e..635402f 100644 --- a/src/js/logic/flatten-pdf-page.ts +++ b/src/js/logic/flatten-pdf-page.ts @@ -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(); 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); diff --git a/src/js/logic/font-to-outline-page.ts b/src/js/logic/font-to-outline-page.ts index 639d192..d5df991 100644 --- a/src/js/logic/font-to-outline-page.ts +++ b/src/js/logic/font-to-outline-page.ts @@ -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(); 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); diff --git a/src/js/logic/linearize-pdf-page.ts b/src/js/logic/linearize-pdf-page.ts index f9db90f..9c227dd 100644 --- a/src/js/logic/linearize-pdf-page.ts +++ b/src/js/logic/linearize-pdf-page.ts @@ -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 = ''; - 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 = ''; + 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(); + 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(); + }); + } }); diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index 714aa04..195a8ac 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -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'; diff --git a/src/js/logic/pages-to-pdf-page.ts b/src/js/logic/pages-to-pdf-page.ts index 506eced..b0d298b 100644 --- a/src/js/logic/pages-to-pdf-page.ts +++ b/src/js/logic/pages-to-pdf-page.ts @@ -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 = ''; + 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 = ''; - 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(); + + 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(); }); diff --git a/src/js/logic/pdf-to-docx-page.ts b/src/js/logic/pdf-to-docx-page.ts index 101f748..4f6d2f9 100644 --- a/src/js/logic/pdf-to-docx-page.ts +++ b/src/js/logic/pdf-to-docx-page.ts @@ -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(); 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...'); diff --git a/src/js/logic/pdf-to-json.ts b/src/js/logic/pdf-to-json.ts index 3ada998..d353243 100644 --- a/src/js/logic/pdf-to-json.ts +++ b/src/js/logic/pdf-to-json.ts @@ -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(); 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' }); diff --git a/src/js/logic/pdf-to-markdown-page.ts b/src/js/logic/pdf-to-markdown-page.ts index 17143c0..651cc64 100644 --- a/src/js/logic/pdf-to-markdown-page.ts +++ b/src/js/logic/pdf-to-markdown-page.ts @@ -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(); 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...'); diff --git a/src/js/logic/pdf-to-pdfa-page.ts b/src/js/logic/pdf-to-pdfa-page.ts index 07b3a7d..6df051d 100644 --- a/src/js/logic/pdf-to-pdfa-page.ts +++ b/src/js/logic/pdf-to-pdfa-page.ts @@ -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(); 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' }); diff --git a/src/js/logic/pdf-to-text-page.ts b/src/js/logic/pdf-to-text-page.ts index efa00ce..81da55c 100644 --- a/src/js/logic/pdf-to-text-page.ts +++ b/src/js/logic/pdf-to-text-page.ts @@ -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(); 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' }); diff --git a/src/js/logic/pdf-to-zip-page.ts b/src/js/logic/pdf-to-zip-page.ts index 858cc0c..1085bf3 100644 --- a/src/js/logic/pdf-to-zip-page.ts +++ b/src/js/logic/pdf-to-zip-page.ts @@ -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 = ''; - 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 = ''; + 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(); + + 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); + } }); diff --git a/src/js/logic/powerpoint-to-pdf-page.ts b/src/js/logic/powerpoint-to-pdf-page.ts index 130fac5..e45baea 100644 --- a/src/js/logic/powerpoint-to-pdf-page.ts +++ b/src/js/logic/powerpoint-to-pdf-page.ts @@ -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 = ''; - 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 = ''; + 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(); + + 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(); }); diff --git a/src/js/logic/prepare-pdf-for-ai-page.ts b/src/js/logic/prepare-pdf-for-ai-page.ts index e65dd48..80fa970 100644 --- a/src/js/logic/prepare-pdf-for-ai-page.ts +++ b/src/js/logic/prepare-pdf-for-ai-page.ts @@ -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(); 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) { diff --git a/src/js/logic/pub-to-pdf-page.ts b/src/js/logic/pub-to-pdf-page.ts index 1c2e682..73d511b 100644 --- a/src/js/logic/pub-to-pdf-page.ts +++ b/src/js/logic/pub-to-pdf-page.ts @@ -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 = ''; + 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 = ''; - 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(); + 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(); }); diff --git a/src/js/logic/rasterize-pdf-page.ts b/src/js/logic/rasterize-pdf-page.ts index 8949c7b..56251d8 100644 --- a/src/js/logic/rasterize-pdf-page.ts +++ b/src/js/logic/rasterize-pdf-page.ts @@ -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(); 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) { diff --git a/src/js/logic/repair-pdf.ts b/src/js/logic/repair-pdf.ts index 0fd684f..30046e1 100644 --- a/src/js/logic/repair-pdf.ts +++ b/src/js/logic/repair-pdf.ts @@ -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 { - 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(); + 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.' + ); + } } diff --git a/src/js/logic/reverse-pages-page.ts b/src/js/logic/reverse-pages-page.ts index 852efc9..22ea9fb 100644 --- a/src/js/logic/reverse-pages-page.ts +++ b/src/js/logic/reverse-pages-page.ts @@ -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 = ''; - 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 = ''; + 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(); + + 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); + } }); diff --git a/src/js/logic/rtf-to-pdf-page.ts b/src/js/logic/rtf-to-pdf-page.ts index 443fc3e..213b5c9 100644 --- a/src/js/logic/rtf-to-pdf-page.ts +++ b/src/js/logic/rtf-to-pdf-page.ts @@ -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 = ''; - 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 = ''; + 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(); + + 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(); }); diff --git a/src/js/logic/vsd-to-pdf-page.ts b/src/js/logic/vsd-to-pdf-page.ts index 01430d6..ba46601 100644 --- a/src/js/logic/vsd-to-pdf-page.ts +++ b/src/js/logic/vsd-to-pdf-page.ts @@ -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 = ''; + 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 = ''; - 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(); + 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(); }); diff --git a/src/js/logic/word-to-pdf-page.ts b/src/js/logic/word-to-pdf-page.ts index e198e3b..933075a 100644 --- a/src/js/logic/word-to-pdf-page.ts +++ b/src/js/logic/word-to-pdf-page.ts @@ -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 = ''; - 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 = ''; + 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(); + + 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(); }); diff --git a/src/js/logic/wpd-to-pdf-page.ts b/src/js/logic/wpd-to-pdf-page.ts index 9d41536..31200ea 100644 --- a/src/js/logic/wpd-to-pdf-page.ts +++ b/src/js/logic/wpd-to-pdf-page.ts @@ -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 = ''; + 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 = ''; - 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(); + + 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(); }); diff --git a/src/js/logic/wps-to-pdf-page.ts b/src/js/logic/wps-to-pdf-page.ts index 641bae2..b101dcb 100644 --- a/src/js/logic/wps-to-pdf-page.ts +++ b/src/js/logic/wps-to-pdf-page.ts @@ -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 = ''; + 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 = ''; - 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(); + + 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(); }); diff --git a/src/js/logic/xml-to-pdf-page.ts b/src/js/logic/xml-to-pdf-page.ts index 382ab5a..dc4b5cf 100644 --- a/src/js/logic/xml-to-pdf-page.ts +++ b/src/js/logic/xml-to-pdf-page.ts @@ -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 = ''; + 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(); - 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 = ''; - 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); + } }); diff --git a/src/js/logic/xps-to-pdf-page.ts b/src/js/logic/xps-to-pdf-page.ts index a0af021..0632d4d 100644 --- a/src/js/logic/xps-to-pdf-page.ts +++ b/src/js/logic/xps-to-pdf-page.ts @@ -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(); 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' }); diff --git a/src/js/utils/deduplicate-filename.ts b/src/js/utils/deduplicate-filename.ts new file mode 100644 index 0000000..a99341a --- /dev/null +++ b/src/js/utils/deduplicate-filename.ts @@ -0,0 +1,28 @@ +export function deduplicateFileName( + name: string, + usedNames: Set +): 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}`; +} diff --git a/src/tests/deduplicate-filename.test.ts b/src/tests/deduplicate-filename.test.ts new file mode 100644 index 0000000..2875799 --- /dev/null +++ b/src/tests/deduplicate-filename.test.ts @@ -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(); + expect(deduplicateFileName('report.pdf', usedNames)).toBe('report.pdf'); + }); + + it('should add the name to the Set after returning', () => { + const usedNames = new Set(); + deduplicateFileName('report.pdf', usedNames); + expect(usedNames.has('report.pdf')).toBe(true); + }); + + it('should append (1) for the first duplicate', () => { + const usedNames = new Set(); + deduplicateFileName('report.pdf', usedNames); + expect(deduplicateFileName('report.pdf', usedNames)).toBe('report (1).pdf'); + }); + + it('should increment counter for multiple duplicates', () => { + const usedNames = new Set(); + 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(); + 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(); + 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(); + 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(); + expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore'); + expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore (1)'); + }); + + it('should handle dotfiles with extension', () => { + const usedNames = new Set(); + 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(); + expect(deduplicateFileName('', usedNames)).toBe(''); + expect(deduplicateFileName('', usedNames)).toBe(' (1)'); + }); + + it('should handle extension-only name', () => { + const usedNames = new Set(); + expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf'); + expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf (1)'); + }); + + it('should preserve the original extension exactly', () => { + const usedNames = new Set(); + 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(); + 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(); + 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(); + expect(deduplicateFileName('レポート.pdf', usedNames)).toBe('レポート.pdf'); + expect(deduplicateFileName('レポート.pdf', usedNames)).toBe( + 'レポート (1).pdf' + ); + }); + + it('should handle names with spaces', () => { + const usedNames = new Set(); + 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(); + 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(); + deduplicateFileName('file.pdf', batch1); + deduplicateFileName('file.pdf', batch1); + + const batch2 = new Set(); + expect(deduplicateFileName('file.pdf', batch2)).toBe('file.pdf'); + }); + + it('should handle many duplicates without infinite loop', () => { + const usedNames = new Set(); + 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(['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(); + 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'); + }); +});