From 84848ab90291e576377f746bdc4d246163fc57cf Mon Sep 17 00:00:00 2001 From: alam00000 Date: Wed, 4 Mar 2026 00:38:07 +0530 Subject: [PATCH] feat: add Quick Look page preview and unify thumbnail styles across all tools --- src/js/logic/delete-pages-page.ts | 2 +- src/js/logic/merge-pdf-page.ts | 3 + src/js/logic/organize-pdf-page.ts | 600 ++++++++------- src/js/logic/pdf-booklet-page.ts | 967 +++++++++++++----------- src/js/logic/remove-blank-pages-page.ts | 29 +- src/js/logic/split-pdf-page.ts | 25 +- src/js/types/index.ts | 1 + src/js/types/page-preview-type.ts | 10 + src/js/ui.ts | 723 +++++++++--------- src/js/utils/page-preview.ts | 215 ++++++ src/js/utils/render-utils.ts | 10 +- src/pages/organize-pdf.html | 17 +- 12 files changed, 1520 insertions(+), 1082 deletions(-) create mode 100644 src/js/types/page-preview-type.ts create mode 100644 src/js/utils/page-preview.ts diff --git a/src/js/logic/delete-pages-page.ts b/src/js/logic/delete-pages-page.ts index 1cecac7..68c576b 100644 --- a/src/js/logic/delete-pages-page.ts +++ b/src/js/logic/delete-pages-page.ts @@ -159,7 +159,7 @@ async function renderThumbnails() { for (let i = 1; i <= deleteState.totalPages; i++) { const page = await deleteState.pdfJsDoc.getPage(i); - const viewport = page.getViewport({ scale: 0.3 }); + const viewport = page.getViewport({ scale: 1 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; diff --git a/src/js/logic/merge-pdf-page.ts b/src/js/logic/merge-pdf-page.ts index e263fd5..714aa04 100644 --- a/src/js/logic/merge-pdf-page.ts +++ b/src/js/logic/merge-pdf-page.ts @@ -9,6 +9,7 @@ import { renderPagesProgressively, cleanupLazyRendering, } from '../utils/render-utils.js'; +import { initPagePreview } from '../utils/page-preview.js'; import { isCpdfAvailable } from '../utils/cpdf-helper.js'; import { showWasmRequiredDialog, @@ -210,6 +211,8 @@ async function renderPageMergeThumbnails() { }, } ); + + initPagePreview(container, pdfjsDoc); } mergeState.cachedThumbnails = true; diff --git a/src/js/logic/organize-pdf-page.ts b/src/js/logic/organize-pdf-page.ts index f0e5828..7ca97ff 100644 --- a/src/js/logic/organize-pdf-page.ts +++ b/src/js/logic/organize-pdf-page.ts @@ -1,365 +1,425 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; +import { + readFileAsArrayBuffer, + formatBytes, + downloadFile, + getPDFDocument, +} from '../utils/helpers.js'; +import { initPagePreview } from '../utils/page-preview.js'; import { PDFDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import Sortable from 'sortablejs'; -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(); interface OrganizeState { - file: File | null; - pdfDoc: any; - pdfJsDoc: any; - totalPages: number; - sortableInstance: any; + file: File | null; + pdfDoc: any; + pdfJsDoc: any; + totalPages: number; + sortableInstance: any; } const organizeState: OrganizeState = { - file: null, - pdfDoc: null, - pdfJsDoc: null, - totalPages: 0, - sortableInstance: null, + file: null, + pdfDoc: null, + pdfJsDoc: null, + totalPages: 0, + sortableInstance: null, }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } function initializePage() { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); - if (fileInput) fileInput.addEventListener('change', handleFileUpload); + if (fileInput) fileInput.addEventListener('change', handleFileUpload); - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); }); - dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); }); - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - const droppedFiles = e.dataTransfer?.files; - if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]); - }); - // Clear value on click to allow re-selecting the same file - fileInput?.addEventListener('click', () => { - if (fileInput) fileInput.value = ''; - }); - } - - if (processBtn) processBtn.addEventListener('click', saveChanges); - - document.getElementById('back-to-tools')?.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); }); + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]); + }); + // Clear value on click to allow re-selecting the same file + fileInput?.addEventListener('click', () => { + if (fileInput) fileInput.value = ''; + }); + } - const applyOrderBtn = document.getElementById('apply-order-btn'); - if (applyOrderBtn) applyOrderBtn.addEventListener('click', applyCustomOrder); + if (processBtn) processBtn.addEventListener('click', saveChanges); + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + + const applyOrderBtn = document.getElementById('apply-order-btn'); + if (applyOrderBtn) applyOrderBtn.addEventListener('click', applyCustomOrder); } function applyCustomOrder() { - const orderInput = document.getElementById('page-order-input') as HTMLInputElement; - const grid = document.getElementById('page-grid'); + const orderInput = document.getElementById( + 'page-order-input' + ) as HTMLInputElement; + const grid = document.getElementById('page-grid'); - if (!orderInput || !grid) return; + if (!orderInput || !grid) return; - const orderString = orderInput.value; - if (!orderString) { - showAlert('Invalid Order', 'Please enter a page order.'); - return; + const orderString = orderInput.value; + if (!orderString) { + showAlert('Invalid Order', 'Please enter a page order.'); + return; + } + + const newOrder = orderString.split(',').map((s) => parseInt(s.trim(), 10)); + + // Validation + const currentGridCount = grid.children.length; + const validNumbers = newOrder.every((n) => !isNaN(n) && n > 0); // Basic check, will validate against available thumbnails + if (!validNumbers) { + showAlert('Invalid Page Numbers', `Please enter positive numbers.`); + return; + } + + if (newOrder.length !== currentGridCount) { + showAlert( + 'Incorrect Page Count', + `The number of pages specified (${newOrder.length}) does not match the current number of pages in the document (${currentGridCount}). Please provide a complete ordering for all pages.` + ); + return; + } + + const uniqueNumbers = new Set(newOrder); + if (uniqueNumbers.size !== newOrder.length) { + showAlert( + 'Duplicate Page Numbers', + 'Please ensure all page numbers in the order are unique.' + ); + return; + } + + const currentThumbnails = Array.from(grid.children) as HTMLElement[]; + const reorderedThumbnails: HTMLElement[] = []; + const foundIndices = new Set(); + + for (const pageNum of newOrder) { + const originalIndexToFind = pageNum - 1; // pageNum is 1-based, originalPageIndex is 0-based + const foundThumbnail = currentThumbnails.find( + (thumb) => + thumb.dataset.originalPageIndex === originalIndexToFind.toString() + ); + + if (foundThumbnail) { + reorderedThumbnails.push(foundThumbnail); + foundIndices.add(originalIndexToFind.toString()); } + } - const newOrder = orderString.split(',').map(s => parseInt(s.trim(), 10)); + const allOriginalIndicesPresent = currentThumbnails.every((thumb) => + foundIndices.has(thumb.dataset.originalPageIndex) + ); - // Validation - const currentGridCount = grid.children.length; - const validNumbers = newOrder.every(n => !isNaN(n) && n > 0); // Basic check, will validate against available thumbnails - if (!validNumbers) { - showAlert('Invalid Page Numbers', `Please enter positive numbers.`); - return; - } + if ( + reorderedThumbnails.length !== currentGridCount || + !allOriginalIndicesPresent + ) { + showAlert( + 'Invalid Page Order', + 'The specified page order is incomplete or contains invalid page numbers. Please ensure you provide a new position for every original page.' + ); + return; + } - if (newOrder.length !== currentGridCount) { - showAlert('Incorrect Page Count', `The number of pages specified (${newOrder.length}) does not match the current number of pages in the document (${currentGridCount}). Please provide a complete ordering for all pages.`); - return; - } + // Clear the grid and append the reordered thumbnails + grid.innerHTML = ''; + reorderedThumbnails.forEach((thumb) => grid.appendChild(thumb)); - const uniqueNumbers = new Set(newOrder); - if (uniqueNumbers.size !== newOrder.length) { - showAlert('Duplicate Page Numbers', 'Please ensure all page numbers in the order are unique.'); - return; - } + initializeSortable(); // Re-initialize sortable on the new order - const currentThumbnails = Array.from(grid.children) as HTMLElement[]; - const reorderedThumbnails: HTMLElement[] = []; - const foundIndices = new Set(); - - for (const pageNum of newOrder) { - const originalIndexToFind = pageNum - 1; // pageNum is 1-based, originalPageIndex is 0-based - const foundThumbnail = currentThumbnails.find( - thumb => thumb.dataset.originalPageIndex === originalIndexToFind.toString() - ); - - if (foundThumbnail) { - reorderedThumbnails.push(foundThumbnail); - foundIndices.add(originalIndexToFind.toString()); - } - } - - const allOriginalIndicesPresent = currentThumbnails.every(thumb => foundIndices.has(thumb.dataset.originalPageIndex)); - - if (reorderedThumbnails.length !== currentGridCount || !allOriginalIndicesPresent) { - showAlert('Invalid Page Order', 'The specified page order is incomplete or contains invalid page numbers. Please ensure you provide a new position for every original page.'); - return; - } - - // Clear the grid and append the reordered thumbnails - grid.innerHTML = ''; - reorderedThumbnails.forEach(thumb => grid.appendChild(thumb)); - - initializeSortable(); // Re-initialize sortable on the new order - - showAlert('Success', 'Pages have been reordered.', 'success'); + showAlert('Success', 'Pages have been reordered.', 'success'); } function handleFileUpload(e: Event) { - const input = e.target as HTMLInputElement; - if (input.files && input.files.length > 0) handleFile(input.files[0]); + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) handleFile(input.files[0]); } async function handleFile(file: File) { - if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if ( + file.type !== 'application/pdf' && + !file.name.toLowerCase().endsWith('.pdf') + ) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } - showLoader('Loading PDF...'); - organizeState.file = file; + showLoader('Loading PDF...'); + organizeState.file = file; - try { - const arrayBuffer = await readFileAsArrayBuffer(file); - organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false }); - organizeState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise; - organizeState.totalPages = organizeState.pdfDoc.getPageCount(); + try { + const arrayBuffer = await readFileAsArrayBuffer(file); + organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { + ignoreEncryption: true, + throwOnInvalidObject: false, + }); + organizeState.pdfJsDoc = await getPDFDocument({ + data: (arrayBuffer as ArrayBuffer).slice(0), + }).promise; + organizeState.totalPages = organizeState.pdfDoc.getPageCount(); - updateFileDisplay(); - await renderThumbnails(); - hideLoader(); - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - } + updateFileDisplay(); + await renderThumbnails(); + hideLoader(); + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + } } function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !organizeState.file) return; + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea || !organizeState.file) return; - fileDisplayArea.innerHTML = ''; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDisplayArea.innerHTML = ''; + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col flex-1 min-w-0'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col flex-1 min-w-0'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = organizeState.file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = organizeState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`; - 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 = () => resetState(); + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => resetState(); - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); } function renumberPages() { - const grid = document.getElementById('page-grid'); - if (!grid) return; - const labels = grid.querySelectorAll('.page-number'); - labels.forEach((label, index) => { - label.textContent = (index + 1).toString(); - }); + const grid = document.getElementById('page-grid'); + if (!grid) return; + const labels = grid.querySelectorAll('.page-number'); + labels.forEach((label, index) => { + label.textContent = (index + 1).toString(); + }); } function attachEventListeners(element: HTMLElement) { - const duplicateBtn = element.querySelector('.duplicate-btn'); - const deleteBtn = element.querySelector('.delete-btn'); + const duplicateBtn = element.querySelector('.duplicate-btn'); + const deleteBtn = element.querySelector('.delete-btn'); - duplicateBtn?.addEventListener('click', (e) => { - e.stopPropagation(); - const clone = element.cloneNode(true) as HTMLElement; - element.after(clone); - attachEventListeners(clone); - renumberPages(); - createIcons({ icons }); - initializeSortable(); - }); + duplicateBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + const clone = element.cloneNode(true) as HTMLElement; + element.after(clone); + attachEventListeners(clone); + renumberPages(); + createIcons({ icons }); + initializeSortable(); + }); - deleteBtn?.addEventListener('click', (e) => { - e.stopPropagation(); - const grid = document.getElementById('page-grid'); - if (grid && grid.children.length > 1) { - element.remove(); - renumberPages(); - initializeSortable(); - } else { - showAlert('Cannot Delete', 'You cannot delete the last page of the document.'); - } - }); + deleteBtn?.addEventListener('click', (e) => { + e.stopPropagation(); + const grid = document.getElementById('page-grid'); + if (grid && grid.children.length > 1) { + element.remove(); + renumberPages(); + initializeSortable(); + } else { + showAlert( + 'Cannot Delete', + 'You cannot delete the last page of the document.' + ); + } + }); } async function renderThumbnails() { - const grid = document.getElementById('page-grid'); - const processBtn = document.getElementById('process-btn'); - const advancedSettings = document.getElementById('advanced-settings'); - if (!grid || !processBtn || !advancedSettings) return; + const grid = document.getElementById('page-grid'); + const processBtn = document.getElementById('process-btn'); + const advancedSettings = document.getElementById('advanced-settings'); + if (!grid || !processBtn || !advancedSettings) return; - grid.innerHTML = ''; - grid.classList.remove('hidden'); - processBtn.classList.remove('hidden'); - advancedSettings.classList.remove('hidden'); + grid.innerHTML = ''; + grid.classList.remove('hidden'); + processBtn.classList.remove('hidden'); + advancedSettings.classList.remove('hidden'); - for (let i = 1; i <= organizeState.totalPages; i++) { - const page = await organizeState.pdfJsDoc.getPage(i); - const viewport = page.getViewport({ scale: 0.5 }); + for (let i = 1; i <= organizeState.totalPages; i++) { + const page = await organizeState.pdfJsDoc.getPage(i); + const viewport = page.getViewport({ scale: 1 }); - const canvas = document.createElement('canvas'); - canvas.width = viewport.width; - canvas.height = viewport.height; - const ctx = canvas.getContext('2d'); - await page.render({ canvasContext: ctx, viewport }).promise; + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const ctx = canvas.getContext('2d'); + await page.render({ canvasContext: ctx, viewport }).promise; - const wrapper = document.createElement('div'); - wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2'; - wrapper.dataset.originalPageIndex = (i - 1).toString(); + 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 group'; + wrapper.dataset.originalPageIndex = (i - 1).toString(); + wrapper.dataset.pageNumber = i.toString(); - const imgContainer = document.createElement('div'); - imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600'; + const imgContainer = document.createElement('div'); + imgContainer.className = 'relative'; - const img = document.createElement('img'); - img.src = canvas.toDataURL(); - img.className = 'max-w-full max-h-full object-contain'; - imgContainer.appendChild(img); + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'rounded-md shadow-md max-w-full h-auto'; + imgContainer.appendChild(img); - const pageLabel = document.createElement('span'); - pageLabel.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; - pageLabel.textContent = i.toString(); + const pageLabel = document.createElement('div'); + pageLabel.className = + 'page-number absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none'; + pageLabel.textContent = i.toString(); + imgContainer.appendChild(pageLabel); - const controlsDiv = document.createElement('div'); - controlsDiv.className = 'flex items-center justify-center gap-4'; + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'flex items-center justify-center gap-4'; - const duplicateBtn = document.createElement('button'); - duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; - duplicateBtn.title = 'Duplicate Page'; - duplicateBtn.innerHTML = ''; + const duplicateBtn = document.createElement('button'); + duplicateBtn.className = + 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; + duplicateBtn.title = 'Duplicate Page'; + duplicateBtn.innerHTML = ''; - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; - deleteBtn.title = 'Delete Page'; - deleteBtn.innerHTML = ''; + const deleteBtn = document.createElement('button'); + deleteBtn.className = + 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; + deleteBtn.title = 'Delete Page'; + deleteBtn.innerHTML = ''; - controlsDiv.append(duplicateBtn, deleteBtn); - wrapper.append(imgContainer, pageLabel, controlsDiv); - grid.appendChild(wrapper); + controlsDiv.append(duplicateBtn, deleteBtn); + wrapper.append(imgContainer, controlsDiv); + grid.appendChild(wrapper); - attachEventListeners(wrapper); - } + attachEventListeners(wrapper); + } - createIcons({ icons }); - initializeSortable(); + createIcons({ icons }); + initializeSortable(); + initPagePreview(grid, organizeState.pdfJsDoc); } function initializeSortable() { - const grid = document.getElementById('page-grid'); - if (!grid) return; + const grid = document.getElementById('page-grid'); + if (!grid) return; - if (organizeState.sortableInstance) organizeState.sortableInstance.destroy(); + if (organizeState.sortableInstance) organizeState.sortableInstance.destroy(); - organizeState.sortableInstance = Sortable.create(grid, { - animation: 150, - ghostClass: 'sortable-ghost', - chosenClass: 'sortable-chosen', - dragClass: 'sortable-drag', - filter: '.duplicate-btn, .delete-btn', - preventOnFilter: true, - onStart: (evt) => { - if (evt.item) evt.item.style.opacity = '0.5'; - }, - onEnd: (evt) => { - if (evt.item) evt.item.style.opacity = '1'; - }, - }); + organizeState.sortableInstance = Sortable.create(grid, { + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + filter: '.duplicate-btn, .delete-btn', + preventOnFilter: true, + onStart: (evt) => { + if (evt.item) evt.item.style.opacity = '0.5'; + }, + onEnd: (evt) => { + if (evt.item) evt.item.style.opacity = '1'; + }, + }); } async function saveChanges() { - showLoader('Building new PDF...'); + showLoader('Building new PDF...'); - try { - const grid = document.getElementById('page-grid'); - if (!grid) return; + try { + const grid = document.getElementById('page-grid'); + if (!grid) return; - const finalPageElements = grid.querySelectorAll('.page-thumbnail'); - const finalIndices = Array.from(finalPageElements) - .map(el => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) - .filter(index => !isNaN(index) && index >= 0); + const finalPageElements = grid.querySelectorAll('.page-thumbnail'); + const finalIndices = Array.from(finalPageElements) + .map((el) => + parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10) + ) + .filter((index) => !isNaN(index) && index >= 0); - if (finalIndices.length === 0) { - showAlert('Error', 'No valid pages to save.'); - return; - } - - const newPdf = await PDFDocument.create(); - const copiedPages = await newPdf.copyPages(organizeState.pdfDoc, finalIndices); - copiedPages.forEach(page => newPdf.addPage(page)); - - const pdfBytes = await newPdf.save(); - const baseName = organizeState.file?.name.replace('.pdf', '') || 'document'; - downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_organized.pdf`); - - hideLoader(); - showAlert('Success', 'PDF organized successfully!', 'success', () => resetState()); - } catch (error) { - console.error('Error saving changes:', error); - hideLoader(); - showAlert('Error', 'Failed to save changes.'); + if (finalIndices.length === 0) { + showAlert('Error', 'No valid pages to save.'); + return; } + + const newPdf = await PDFDocument.create(); + const copiedPages = await newPdf.copyPages( + organizeState.pdfDoc, + finalIndices + ); + copiedPages.forEach((page) => newPdf.addPage(page)); + + const pdfBytes = await newPdf.save(); + const baseName = organizeState.file?.name.replace('.pdf', '') || 'document'; + downloadFile( + new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), + `${baseName}_organized.pdf` + ); + + hideLoader(); + showAlert('Success', 'PDF organized successfully!', 'success', () => + resetState() + ); + } catch (error) { + console.error('Error saving changes:', error); + hideLoader(); + showAlert('Error', 'Failed to save changes.'); + } } function resetState() { - if (organizeState.sortableInstance) { - organizeState.sortableInstance.destroy(); - organizeState.sortableInstance = null; - } + if (organizeState.sortableInstance) { + organizeState.sortableInstance.destroy(); + organizeState.sortableInstance = null; + } - organizeState.file = null; - organizeState.pdfDoc = null; - organizeState.pdfJsDoc = null; - organizeState.totalPages = 0; + organizeState.file = null; + organizeState.pdfDoc = null; + organizeState.pdfJsDoc = null; + organizeState.totalPages = 0; - const grid = document.getElementById('page-grid'); - if (grid) { - grid.innerHTML = ''; - grid.classList.add('hidden'); - } - document.getElementById('process-btn')?.classList.add('hidden'); - document.getElementById('advanced-settings')?.classList.add('hidden'); - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + const grid = document.getElementById('page-grid'); + if (grid) { + grid.innerHTML = ''; + grid.classList.add('hidden'); + } + document.getElementById('process-btn')?.classList.add('hidden'); + document.getElementById('advanced-settings')?.classList.add('hidden'); + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; } diff --git a/src/js/logic/pdf-booklet-page.ts b/src/js/logic/pdf-booklet-page.ts index 489ba45..0fbca6b 100644 --- a/src/js/logic/pdf-booklet-page.ts +++ b/src/js/logic/pdf-booklet-page.ts @@ -5,513 +5,618 @@ import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( - 'pdfjs-dist/build/pdf.worker.mjs', - import.meta.url + 'pdfjs-dist/build/pdf.worker.mjs', + import.meta.url ).toString(); interface BookletState { - file: File | null; - pdfDoc: PDFLibDocument | null; - pdfBytes: Uint8Array | null; - pdfjsDoc: pdfjsLib.PDFDocumentProxy | null; + file: File | null; + pdfDoc: PDFLibDocument | null; + pdfBytes: Uint8Array | null; + pdfjsDoc: pdfjsLib.PDFDocumentProxy | null; } const pageState: BookletState = { - file: null, - pdfDoc: null, - pdfBytes: null, - pdfjsDoc: null, + file: null, + pdfDoc: null, + pdfBytes: null, + pdfjsDoc: null, }; function resetState() { - pageState.file = null; - pageState.pdfDoc = null; - pageState.pdfBytes = null; - pageState.pdfjsDoc = null; + pageState.file = null; + pageState.pdfDoc = null; + pageState.pdfBytes = null; + pageState.pdfjsDoc = null; - 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 = ''; - const previewArea = document.getElementById('booklet-preview'); - if (previewArea) previewArea.innerHTML = '

Upload a PDF and click "Generate Preview" to see the booklet layout

'; + const previewArea = document.getElementById('booklet-preview'); + if (previewArea) + previewArea.innerHTML = + '

Upload a PDF and click "Generate Preview" to see the booklet layout

'; - const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement; - if (downloadBtn) downloadBtn.disabled = true; + const downloadBtn = document.getElementById( + 'download-btn' + ) as HTMLButtonElement; + if (downloadBtn) downloadBtn.disabled = true; } async 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.file) { - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + if (pageState.file) { + 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 = pageState.file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = pageState.file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`; - 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 () { - resetState(); - }; + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = function () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - try { - showLoader('Loading PDF...'); - const arrayBuffer = await pageState.file.arrayBuffer(); - pageState.pdfBytes = new Uint8Array(arrayBuffer); + try { + showLoader('Loading PDF...'); + const arrayBuffer = await pageState.file.arrayBuffer(); + pageState.pdfBytes = new Uint8Array(arrayBuffer); - pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, { - ignoreEncryption: true, - throwOnInvalidObject: false - }); + pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, { + ignoreEncryption: true, + throwOnInvalidObject: false, + }); - pageState.pdfjsDoc = await pdfjsLib.getDocument({ data: pageState.pdfBytes.slice() }).promise; + pageState.pdfjsDoc = await pdfjsLib.getDocument({ + data: pageState.pdfBytes.slice(), + }).promise; - hideLoader(); + hideLoader(); - const pageCount = pageState.pdfDoc.getPageCount(); - metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; + const pageCount = pageState.pdfDoc.getPageCount(); + metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`; - if (toolOptions) toolOptions.classList.remove('hidden'); + if (toolOptions) toolOptions.classList.remove('hidden'); - const previewBtn = document.getElementById('preview-btn') as HTMLButtonElement; - if (previewBtn) previewBtn.disabled = false; - } catch (error) { - console.error('Error loading PDF:', error); - hideLoader(); - showAlert('Error', 'Failed to load PDF file.'); - resetState(); - } - } else { - if (toolOptions) toolOptions.classList.add('hidden'); + const previewBtn = document.getElementById( + 'preview-btn' + ) as HTMLButtonElement; + if (previewBtn) previewBtn.disabled = false; + } catch (error) { + console.error('Error loading PDF:', error); + hideLoader(); + showAlert('Error', 'Failed to load PDF file.'); + resetState(); } + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } function getGridDimensions(): { rows: number; cols: number } { - const gridMode = (document.querySelector('input[name="grid-mode"]:checked') as HTMLInputElement)?.value || '1x2'; - switch (gridMode) { - case '1x2': return { rows: 1, cols: 2 }; - case '2x2': return { rows: 2, cols: 2 }; - case '2x4': return { rows: 2, cols: 4 }; - case '4x4': return { rows: 4, cols: 4 }; - default: return { rows: 1, cols: 2 }; - } + const gridMode = + ( + document.querySelector( + 'input[name="grid-mode"]:checked' + ) as HTMLInputElement + )?.value || '1x2'; + switch (gridMode) { + case '1x2': + return { rows: 1, cols: 2 }; + case '2x2': + return { rows: 2, cols: 2 }; + case '2x4': + return { rows: 2, cols: 4 }; + case '4x4': + return { rows: 4, cols: 4 }; + default: + return { rows: 1, cols: 2 }; + } } function getOrientation(isBookletMode: boolean): 'portrait' | 'landscape' { - const orientationValue = (document.querySelector('input[name="orientation"]:checked') as HTMLInputElement)?.value || 'auto'; - if (orientationValue === 'portrait') return 'portrait'; - if (orientationValue === 'landscape') return 'landscape'; - return isBookletMode ? 'landscape' : 'portrait'; + const orientationValue = + ( + document.querySelector( + 'input[name="orientation"]:checked' + ) as HTMLInputElement + )?.value || 'auto'; + if (orientationValue === 'portrait') return 'portrait'; + if (orientationValue === 'landscape') return 'landscape'; + return isBookletMode ? 'landscape' : 'portrait'; } -function getSheetDimensions(isBookletMode: boolean): { width: number; height: number } { - const paperSizeKey = (document.getElementById('paper-size') as HTMLSelectElement).value as keyof typeof PageSizes; - const pageDims = PageSizes[paperSizeKey] || PageSizes.Letter; - const orientation = getOrientation(isBookletMode); - if (orientation === 'landscape') { - return { width: pageDims[1], height: pageDims[0] }; - } - return { width: pageDims[0], height: pageDims[1] }; +function getSheetDimensions(isBookletMode: boolean): { + width: number; + height: number; +} { + const paperSizeKey = ( + document.getElementById('paper-size') as HTMLSelectElement + ).value as keyof typeof PageSizes; + const pageDims = PageSizes[paperSizeKey] || PageSizes.Letter; + const orientation = getOrientation(isBookletMode); + if (orientation === 'landscape') { + return { width: pageDims[1], height: pageDims[0] }; + } + return { width: pageDims[0], height: pageDims[1] }; } async function generatePreview() { - if (!pageState.pdfDoc || !pageState.pdfjsDoc) { - showAlert('Error', 'Please load a PDF first.'); - return; + if (!pageState.pdfDoc || !pageState.pdfjsDoc) { + showAlert('Error', 'Please load a PDF first.'); + return; + } + + const previewArea = document.getElementById('booklet-preview')!; + const totalPages = pageState.pdfDoc.getPageCount(); + const { rows, cols } = getGridDimensions(); + const pagesPerSheet = rows * cols; + const isBookletMode = rows === 1 && cols === 2; + + let numSheets: number; + if (isBookletMode) { + const sheetsNeeded = Math.ceil(totalPages / 4); + numSheets = sheetsNeeded * 2; + } else { + numSheets = Math.ceil(totalPages / pagesPerSheet); + } + + const { width: sheetWidth, height: sheetHeight } = + getSheetDimensions(isBookletMode); + + // Get container width to make canvas fill it + const previewContainer = document.getElementById('booklet-preview')!; + const containerWidth = previewContainer.clientWidth - 32; // account for padding + const aspectRatio = sheetWidth / sheetHeight; + const canvasWidth = containerWidth; + const canvasHeight = containerWidth / aspectRatio; + + previewArea.innerHTML = + '

Generating preview...

'; + + const totalRounded = isBookletMode + ? Math.ceil(totalPages / 4) * 4 + : totalPages; + const rotationMode = + ( + document.querySelector( + 'input[name="rotation"]:checked' + ) as HTMLInputElement + )?.value || 'none'; + + const pageThumbnails: Map = new Map(); + const thumbnailScale = 1; + + for (let i = 1; i <= totalPages; i++) { + try { + const page = await pageState.pdfjsDoc.getPage(i); + const viewport = page.getViewport({ scale: thumbnailScale }); + + const offscreen = new OffscreenCanvas(viewport.width, viewport.height); + const ctx = offscreen.getContext('2d')!; + + await page.render({ + canvasContext: ctx as any, + viewport: viewport, + canvas: offscreen as any, + }).promise; + + const bitmap = await createImageBitmap(offscreen); + pageThumbnails.set(i, bitmap); + } catch (e) { + console.error(`Failed to render page ${i}:`, e); + } + } + + previewArea.innerHTML = `

${totalPages} pages → ${numSheets} output sheets

`; + + for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + canvas.className = 'border border-gray-600 rounded-lg mb-4'; + + const ctx = canvas.getContext('2d')!; + + const isFront = sheetIndex % 2 === 0; + ctx.fillStyle = isFront ? '#1f2937' : '#1a2e1a'; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + + ctx.strokeStyle = '#4b5563'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvasWidth, canvasHeight); + + const cellWidth = canvasWidth / cols; + const cellHeight = canvasHeight / rows; + const padding = 4; + + ctx.strokeStyle = '#374151'; + ctx.lineWidth = 1; + ctx.setLineDash([2, 2]); + for (let c = 1; c < cols; c++) { + ctx.beginPath(); + ctx.moveTo(c * cellWidth, 0); + ctx.lineTo(c * cellWidth, canvasHeight); + ctx.stroke(); + } + for (let r = 1; r < rows; r++) { + ctx.beginPath(); + ctx.moveTo(0, r * cellHeight); + ctx.lineTo(canvasWidth, r * cellHeight); + ctx.stroke(); + } + ctx.setLineDash([]); + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const slotIndex = r * cols + c; + let pageNumber: number; + + if (isBookletMode) { + const physicalSheet = Math.floor(sheetIndex / 2); + const isFrontSide = sheetIndex % 2 === 0; + if (isFrontSide) { + pageNumber = + c === 0 + ? totalRounded - 2 * physicalSheet + : 2 * physicalSheet + 1; + } else { + pageNumber = + c === 0 + ? 2 * physicalSheet + 2 + : totalRounded - 2 * physicalSheet - 1; + } + } else { + pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1; + } + + const x = c * cellWidth + padding; + const y = r * cellHeight + padding; + const slotWidth = cellWidth - padding * 2; + const slotHeight = cellHeight - padding * 2; + + const exists = pageNumber >= 1 && pageNumber <= totalPages; + + if (exists) { + const thumbnail = pageThumbnails.get(pageNumber); + if (thumbnail) { + let rotation = 0; + if (rotationMode === '90cw') rotation = 90; + else if (rotationMode === '90ccw') rotation = -90; + else if (rotationMode === 'alternate') + rotation = pageNumber % 2 === 1 ? 90 : -90; + + const isRotated = rotation !== 0; + const srcWidth = isRotated ? thumbnail.height : thumbnail.width; + const srcHeight = isRotated ? thumbnail.width : thumbnail.height; + const scale = Math.min( + slotWidth / srcWidth, + slotHeight / srcHeight + ); + const drawWidth = srcWidth * scale; + const drawHeight = srcHeight * scale; + const drawX = x + (slotWidth - drawWidth) / 2; + const drawY = y + (slotHeight - drawHeight) / 2; + + ctx.save(); + if (rotation !== 0) { + const centerX = drawX + drawWidth / 2; + const centerY = drawY + drawHeight / 2; + ctx.translate(centerX, centerY); + ctx.rotate((rotation * Math.PI) / 180); + ctx.drawImage( + thumbnail, + -drawHeight / 2, + -drawWidth / 2, + drawHeight, + drawWidth + ); + } else { + ctx.drawImage(thumbnail, drawX, drawY, drawWidth, drawHeight); + } + ctx.restore(); + + ctx.strokeStyle = '#6b7280'; + ctx.lineWidth = 1; + ctx.strokeRect(drawX, drawY, drawWidth, drawHeight); + + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText( + `${pageNumber}`, + x + slotWidth / 2, + y + slotHeight - 4 + ); + } + } else { + ctx.fillStyle = '#374151'; + ctx.fillRect(x, y, slotWidth, slotHeight); + ctx.strokeStyle = '#4b5563'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, slotWidth, slotHeight); + + ctx.fillStyle = '#6b7280'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('(blank)', x + slotWidth / 2, y + slotHeight / 2); + } + } } - const previewArea = document.getElementById('booklet-preview')!; - const totalPages = pageState.pdfDoc.getPageCount(); + ctx.fillStyle = '#9ca3af'; + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : ''; + ctx.fillText( + `Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`, + canvasWidth - 6, + 4 + ); + + previewArea.appendChild(canvas); + } + + pageThumbnails.forEach((bitmap) => bitmap.close()); + + const downloadBtn = document.getElementById( + 'download-btn' + ) as HTMLButtonElement; + downloadBtn.disabled = false; +} + +function applyRotation(doc: PDFLibDocument, mode: string) { + const pages = doc.getPages(); + pages.forEach((page, index) => { + let rotation: number; + switch (mode) { + case '90cw': + rotation = 90; + break; + case '90ccw': + rotation = -90; + break; + case 'alternate': + rotation = index % 2 === 0 ? 90 : -90; + break; + default: + rotation = 0; + } + if (rotation !== 0) { + page.setRotation(degrees(page.getRotation().angle + rotation)); + } + }); +} + +async function createBooklet() { + if (!pageState.pdfBytes) { + showAlert('Error', 'Please load a PDF first.'); + return; + } + + showLoader('Creating Booklet...'); + + try { + const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice()); + const rotationMode = + ( + document.querySelector( + 'input[name="rotation"]:checked' + ) as HTMLInputElement + )?.value || 'none'; + applyRotation(sourceDoc, rotationMode); + + const totalPages = sourceDoc.getPageCount(); const { rows, cols } = getGridDimensions(); const pagesPerSheet = rows * cols; const isBookletMode = rows === 1 && cols === 2; + const { width: sheetWidth, height: sheetHeight } = + getSheetDimensions(isBookletMode); + + const outputDoc = await PDFLibDocument.create(); + let numSheets: number; + let totalRounded: number; if (isBookletMode) { - const sheetsNeeded = Math.ceil(totalPages / 4); - numSheets = sheetsNeeded * 2; + totalRounded = Math.ceil(totalPages / 4) * 4; + numSheets = Math.ceil(totalPages / 4) * 2; } else { - numSheets = Math.ceil(totalPages / pagesPerSheet); + totalRounded = totalPages; + numSheets = Math.ceil(totalPages / pagesPerSheet); } - const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode); - - // Get container width to make canvas fill it - const previewContainer = document.getElementById('booklet-preview')!; - const containerWidth = previewContainer.clientWidth - 32; // account for padding - const aspectRatio = sheetWidth / sheetHeight; - const canvasWidth = containerWidth; - const canvasHeight = containerWidth / aspectRatio; - - previewArea.innerHTML = '

Generating preview...

'; - - const totalRounded = isBookletMode ? Math.ceil(totalPages / 4) * 4 : totalPages; - const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none'; - - const pageThumbnails: Map = new Map(); - const thumbnailScale = 0.3; - - for (let i = 1; i <= totalPages; i++) { - try { - const page = await pageState.pdfjsDoc.getPage(i); - const viewport = page.getViewport({ scale: thumbnailScale }); - - const offscreen = new OffscreenCanvas(viewport.width, viewport.height); - const ctx = offscreen.getContext('2d')!; - - await page.render({ - canvasContext: ctx as any, - viewport: viewport, - canvas: offscreen as any, - }).promise; - - const bitmap = await createImageBitmap(offscreen); - pageThumbnails.set(i, bitmap); - } catch (e) { - console.error(`Failed to render page ${i}:`, e); - } - } - - previewArea.innerHTML = `

${totalPages} pages → ${numSheets} output sheets

`; + const cellWidth = sheetWidth / cols; + const cellHeight = sheetHeight / rows; + const padding = 10; for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) { - const canvas = document.createElement('canvas'); - canvas.width = canvasWidth; - canvas.height = canvasHeight; - canvas.className = 'border border-gray-600 rounded-lg mb-4'; + const outputPage = outputDoc.addPage([sheetWidth, sheetHeight]); - const ctx = canvas.getContext('2d')!; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const slotIndex = r * cols + c; + let pageNumber: number; - const isFront = sheetIndex % 2 === 0; - ctx.fillStyle = isFront ? '#1f2937' : '#1a2e1a'; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); - - ctx.strokeStyle = '#4b5563'; - ctx.lineWidth = 1; - ctx.strokeRect(0, 0, canvasWidth, canvasHeight); - - const cellWidth = canvasWidth / cols; - const cellHeight = canvasHeight / rows; - const padding = 4; - - ctx.strokeStyle = '#374151'; - ctx.lineWidth = 1; - ctx.setLineDash([2, 2]); - for (let c = 1; c < cols; c++) { - ctx.beginPath(); - ctx.moveTo(c * cellWidth, 0); - ctx.lineTo(c * cellWidth, canvasHeight); - ctx.stroke(); - } - for (let r = 1; r < rows; r++) { - ctx.beginPath(); - ctx.moveTo(0, r * cellHeight); - ctx.lineTo(canvasWidth, r * cellHeight); - ctx.stroke(); - } - ctx.setLineDash([]); - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const slotIndex = r * cols + c; - let pageNumber: number; - - if (isBookletMode) { - const physicalSheet = Math.floor(sheetIndex / 2); - const isFrontSide = sheetIndex % 2 === 0; - if (isFrontSide) { - pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1; - } else { - pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1; - } - } else { - pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1; - } - - const x = c * cellWidth + padding; - const y = r * cellHeight + padding; - const slotWidth = cellWidth - padding * 2; - const slotHeight = cellHeight - padding * 2; - - const exists = pageNumber >= 1 && pageNumber <= totalPages; - - if (exists) { - const thumbnail = pageThumbnails.get(pageNumber); - if (thumbnail) { - let rotation = 0; - if (rotationMode === '90cw') rotation = 90; - else if (rotationMode === '90ccw') rotation = -90; - else if (rotationMode === 'alternate') rotation = (pageNumber % 2 === 1) ? 90 : -90; - - const isRotated = rotation !== 0; - const srcWidth = isRotated ? thumbnail.height : thumbnail.width; - const srcHeight = isRotated ? thumbnail.width : thumbnail.height; - const scale = Math.min(slotWidth / srcWidth, slotHeight / srcHeight); - const drawWidth = srcWidth * scale; - const drawHeight = srcHeight * scale; - const drawX = x + (slotWidth - drawWidth) / 2; - const drawY = y + (slotHeight - drawHeight) / 2; - - ctx.save(); - if (rotation !== 0) { - const centerX = drawX + drawWidth / 2; - const centerY = drawY + drawHeight / 2; - ctx.translate(centerX, centerY); - ctx.rotate((rotation * Math.PI) / 180); - ctx.drawImage(thumbnail, -drawHeight / 2, -drawWidth / 2, drawHeight, drawWidth); - } else { - ctx.drawImage(thumbnail, drawX, drawY, drawWidth, drawHeight); - } - ctx.restore(); - - ctx.strokeStyle = '#6b7280'; - ctx.lineWidth = 1; - ctx.strokeRect(drawX, drawY, drawWidth, drawHeight); - - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(`${pageNumber}`, x + slotWidth / 2, y + slotHeight - 4); - } - } else { - ctx.fillStyle = '#374151'; - ctx.fillRect(x, y, slotWidth, slotHeight); - ctx.strokeStyle = '#4b5563'; - ctx.lineWidth = 1; - ctx.strokeRect(x, y, slotWidth, slotHeight); - - ctx.fillStyle = '#6b7280'; - ctx.font = '10px sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText('(blank)', x + slotWidth / 2, y + slotHeight / 2); - } + if (isBookletMode) { + const physicalSheet = Math.floor(sheetIndex / 2); + const isFrontSide = sheetIndex % 2 === 0; + if (isFrontSide) { + pageNumber = + c === 0 + ? totalRounded - 2 * physicalSheet + : 2 * physicalSheet + 1; + } else { + pageNumber = + c === 0 + ? 2 * physicalSheet + 2 + : totalRounded - 2 * physicalSheet - 1; } + } else { + pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1; + } + + if (pageNumber >= 1 && pageNumber <= totalPages) { + const [embeddedPage] = await outputDoc.embedPdf(sourceDoc, [ + pageNumber - 1, + ]); + const { width: srcW, height: srcH } = embeddedPage; + + const availableWidth = cellWidth - padding * 2; + const availableHeight = cellHeight - padding * 2; + const scale = Math.min( + availableWidth / srcW, + availableHeight / srcH + ); + + const scaledWidth = srcW * scale; + const scaledHeight = srcH * scale; + + const x = + c * cellWidth + padding + (availableWidth - scaledWidth) / 2; + const y = + sheetHeight - + (r + 1) * cellHeight + + padding + + (availableHeight - scaledHeight) / 2; + + outputPage.drawPage(embeddedPage, { + x, + y, + width: scaledWidth, + height: scaledHeight, + }); + } } - - ctx.fillStyle = '#9ca3af'; - ctx.font = 'bold 10px sans-serif'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : ''; - ctx.fillText(`Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`, canvasWidth - 6, 4); - - previewArea.appendChild(canvas); + } } - pageThumbnails.forEach(bitmap => bitmap.close()); + const pdfBytes = await outputDoc.save(); + const originalName = + pageState.file?.name.replace(/\.pdf$/i, '') || 'document'; - const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement; - downloadBtn.disabled = false; -} + downloadFile( + new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), + `${originalName}_booklet.pdf` + ); -function applyRotation(doc: PDFLibDocument, mode: string) { - const pages = doc.getPages(); - pages.forEach((page, index) => { - let rotation = 0; - switch (mode) { - case '90cw': rotation = 90; break; - case '90ccw': rotation = -90; break; - case 'alternate': rotation = (index % 2 === 0) ? 90 : -90; break; - default: rotation = 0; - } - if (rotation !== 0) { - page.setRotation(degrees(page.getRotation().angle + rotation)); - } - }); -} - -async function createBooklet() { - if (!pageState.pdfBytes) { - showAlert('Error', 'Please load a PDF first.'); - return; - } - - showLoader('Creating Booklet...'); - - try { - const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice()); - const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none'; - applyRotation(sourceDoc, rotationMode); - - const totalPages = sourceDoc.getPageCount(); - const { rows, cols } = getGridDimensions(); - const pagesPerSheet = rows * cols; - const isBookletMode = rows === 1 && cols === 2; - - const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode); - - const outputDoc = await PDFLibDocument.create(); - - let numSheets: number; - let totalRounded: number; - if (isBookletMode) { - totalRounded = Math.ceil(totalPages / 4) * 4; - numSheets = Math.ceil(totalPages / 4) * 2; - } else { - totalRounded = totalPages; - numSheets = Math.ceil(totalPages / pagesPerSheet); - } - - const cellWidth = sheetWidth / cols; - const cellHeight = sheetHeight / rows; - const padding = 10; - - for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) { - const outputPage = outputDoc.addPage([sheetWidth, sheetHeight]); - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const slotIndex = r * cols + c; - let pageNumber: number; - - if (isBookletMode) { - const physicalSheet = Math.floor(sheetIndex / 2); - const isFrontSide = sheetIndex % 2 === 0; - if (isFrontSide) { - pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1; - } else { - pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1; - } - } else { - pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1; - } - - if (pageNumber >= 1 && pageNumber <= totalPages) { - const [embeddedPage] = await outputDoc.embedPdf(sourceDoc, [pageNumber - 1]); - const { width: srcW, height: srcH } = embeddedPage; - - const availableWidth = cellWidth - padding * 2; - const availableHeight = cellHeight - padding * 2; - const scale = Math.min(availableWidth / srcW, availableHeight / srcH); - - const scaledWidth = srcW * scale; - const scaledHeight = srcH * scale; - - const x = c * cellWidth + padding + (availableWidth - scaledWidth) / 2; - const y = sheetHeight - (r + 1) * cellHeight + padding + (availableHeight - scaledHeight) / 2; - - outputPage.drawPage(embeddedPage, { - x, - y, - width: scaledWidth, - height: scaledHeight, - }); - } - } - } - } - - const pdfBytes = await outputDoc.save(); - const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document'; - - downloadFile( - new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), - `${originalName}_booklet.pdf` - ); - - showAlert('Success', `Booklet created with ${numSheets} sheets!`, 'success', function () { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while creating the booklet.'); - } finally { - hideLoader(); - } + showAlert( + 'Success', + `Booklet created with ${numSheets} sheets!`, + 'success', + function () { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while creating the booklet.'); + } finally { + hideLoader(); + } } function handleFileSelect(files: FileList | null) { - if (files && files.length > 0) { - const file = files[0]; - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { - pageState.file = file; - updateUI(); - } + if (files && files.length > 0) { + const file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const previewBtn = document.getElementById('preview-btn'); - const downloadBtn = document.getElementById('download-btn'); - const backBtn = document.getElementById('back-to-tools'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const previewBtn = document.getElementById('preview-btn'); + const downloadBtn = document.getElementById('download-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); + }); + + 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('drop', function (e) { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const files = e.dataTransfer?.files; + 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) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(pdfFiles[0]); + handleFileSelect(dataTransfer.files); + } + } + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', function (e) { - handleFileSelect((e.target as HTMLInputElement).files); - }); + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - dropZone.addEventListener('dragover', function (e) { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + if (previewBtn) { + previewBtn.addEventListener('click', generatePreview); + } - 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'); - const files = e.dataTransfer?.files; - 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) { - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(pdfFiles[0]); - handleFileSelect(dataTransfer.files); - } - } - }); - - fileInput.addEventListener('click', function () { - fileInput.value = ''; - }); - } - - if (previewBtn) { - previewBtn.addEventListener('click', generatePreview); - } - - if (downloadBtn) { - downloadBtn.addEventListener('click', createBooklet); - } + if (downloadBtn) { + downloadBtn.addEventListener('click', createBooklet); + } }); diff --git a/src/js/logic/remove-blank-pages-page.ts b/src/js/logic/remove-blank-pages-page.ts index d18dede..dea1fa8 100644 --- a/src/js/logic/remove-blank-pages-page.ts +++ b/src/js/logic/remove-blank-pages-page.ts @@ -1,6 +1,7 @@ import { PDFDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import { createIcons, icons } from 'lucide'; +import { initPagePreview } from '../utils/page-preview.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -161,7 +162,7 @@ async function isPageBlank( } async function generateThumbnail(page: any): Promise { - const viewport = page.getViewport({ scale: 1.5 }); + const viewport = page.getViewport({ scale: 1 }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return ''; @@ -211,6 +212,10 @@ async function detectBlankPages() { // Show preview panel updatePreviewPanel(); document.getElementById('preview-panel')?.classList.remove('hidden'); + + const previewContainer = document.getElementById('blank-pages-preview'); + if (previewContainer) initPagePreview(previewContainer, pdfDoc); + hideLoader(); } catch (e) { console.error(e); @@ -231,17 +236,18 @@ function updatePreviewPanel() { pageState.detectedBlankPages.forEach((pageIndex) => { const thumbnail = pageState.pageThumbnails.get(pageIndex) || ''; const div = document.createElement('div'); - div.className = 'relative cursor-pointer group'; + div.className = + 'relative cursor-pointer flex flex-col items-center gap-1 p-2 border-2 border-red-500 rounded-lg bg-gray-700 transition-colors group'; div.dataset.pageIndex = String(pageIndex); div.dataset.selected = 'true'; div.innerHTML = ` -
- Page ${pageIndex + 1} -
- Page ${pageIndex + 1} +
+ Page ${pageIndex + 1} +
+ ${pageIndex + 1}
-
+
@@ -256,18 +262,17 @@ function updatePreviewPanel() { function togglePageSelection(div: HTMLElement, pageIndex: number) { const isSelected = div.dataset.selected === 'true'; - const border = div.querySelector('.border-2') as HTMLElement; const checkMark = div.querySelector('.check-mark') as HTMLElement; if (isSelected) { div.dataset.selected = 'false'; - border?.classList.remove('border-red-500'); - border?.classList.add('border-gray-500', 'opacity-50'); + div.classList.remove('border-red-500'); + div.classList.add('border-gray-600', 'opacity-50'); checkMark?.classList.add('hidden'); } else { div.dataset.selected = 'true'; - border?.classList.add('border-red-500'); - border?.classList.remove('border-gray-500', 'opacity-50'); + div.classList.add('border-red-500'); + div.classList.remove('border-gray-600', 'opacity-50'); checkMark?.classList.remove('hidden'); } } diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts index 81ac828..699c248 100644 --- a/src/js/logic/split-pdf-page.ts +++ b/src/js/logic/split-pdf-page.ts @@ -12,6 +12,7 @@ import { renderPagesProgressively, cleanupLazyRendering, } from '../utils/render-utils.js'; +import { initPagePreview } from '../utils/page-preview.js'; import { isCpdfAvailable } from '../utils/cpdf-helper.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import JSZip from 'jszip'; @@ -153,18 +154,24 @@ document.addEventListener('DOMContentLoaded', () => { const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const wrapper = document.createElement('div'); wrapper.className = - 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500 relative'; + 'page-thumbnail-wrapper p-2 border-2 border-gray-600 rounded-lg cursor-pointer hover:border-indigo-500 bg-gray-700 transition-colors relative group flex flex-col items-center gap-1'; wrapper.dataset.pageIndex = (pageNumber - 1).toString(); + wrapper.dataset.pageNumber = pageNumber.toString(); + + const imgContainer = document.createElement('div'); + imgContainer.className = 'relative'; const img = document.createElement('img'); img.src = canvas.toDataURL(); - img.className = 'rounded-md w-full h-auto'; + img.className = 'rounded-md shadow-md max-w-full h-auto'; - const p = document.createElement('p'); - p.className = 'text-center text-xs mt-1 text-gray-300'; - p.textContent = `Page ${pageNumber}`; + const pageNumDiv = document.createElement('div'); + pageNumDiv.className = + 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none'; + pageNumDiv.textContent = pageNumber.toString(); - wrapper.append(img, p); + imgContainer.append(img, pageNumDiv); + wrapper.appendChild(imgContainer); const handleSelection = (e: any) => { e.preventDefault(); @@ -174,10 +181,10 @@ document.addEventListener('DOMContentLoaded', () => { if (isSelected) { wrapper.classList.remove('selected', 'border-indigo-500'); - wrapper.classList.add('border-transparent'); + wrapper.classList.add('border-gray-600'); } else { wrapper.classList.add('selected', 'border-indigo-500'); - wrapper.classList.remove('border-transparent'); + wrapper.classList.remove('border-gray-600'); } }; @@ -203,6 +210,8 @@ document.addEventListener('DOMContentLoaded', () => { createIcons({ icons }); }, }); + + initPagePreview(container, pdf); } catch (error) { console.error('Error rendering visual selector:', error); showAlert('Error', 'Failed to render page previews.'); diff --git a/src/js/types/index.ts b/src/js/types/index.ts index d27c8c6..aa9dfe9 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -50,3 +50,4 @@ export * from './bookmark-pdf-type.ts'; export * from './scanner-effect-type.ts'; export * from './adjust-colors-type.ts'; export * from './bates-numbering-type.ts'; +export * from './page-preview-type.ts'; diff --git a/src/js/types/page-preview-type.ts b/src/js/types/page-preview-type.ts new file mode 100644 index 0000000..3e4f367 --- /dev/null +++ b/src/js/types/page-preview-type.ts @@ -0,0 +1,10 @@ +import { PDFDocumentProxy } from 'pdfjs-dist'; + +export interface PreviewState { + modal: HTMLElement | null; + pdfjsDoc: PDFDocumentProxy | null; + currentPage: number; + totalPages: number; + isOpen: boolean; + container: HTMLElement | null; +} diff --git a/src/js/ui.ts b/src/js/ui.ts index 427dbdb..935459d 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1,177 +1,201 @@ import { resetState } from './state.js'; import { formatBytes, getPDFDocument } from './utils/helpers.js'; import { tesseractLanguages } from './config/tesseract-languages.js'; -import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js'; +import { + renderPagesProgressively, + cleanupLazyRendering, +} from './utils/render-utils.js'; +import { initPagePreview } from './utils/page-preview.js'; import { icons, createIcons } from 'lucide'; import Sortable from 'sortablejs'; -import { getRotationState, updateRotationState } from './utils/rotation-state.js'; +import { + getRotationState, + updateRotationState, +} from './utils/rotation-state.js'; import * as pdfjsLib from 'pdfjs-dist'; import { t } from './i18n/i18n'; -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(); // Centralizing DOM element selection export const dom = { - gridView: document.getElementById('grid-view'), - toolGrid: document.getElementById('tool-grid'), - toolInterface: document.getElementById('tool-interface'), - toolContent: document.getElementById('tool-content'), - backToGridBtn: document.getElementById('back-to-grid'), - loaderModal: document.getElementById('loader-modal'), - loaderText: document.getElementById('loader-text'), - alertModal: document.getElementById('alert-modal'), - alertTitle: document.getElementById('alert-title'), - alertMessage: document.getElementById('alert-message'), - alertOkBtn: document.getElementById('alert-ok'), - heroSection: document.getElementById('hero-section'), - featuresSection: document.getElementById('features-section'), - toolsHeader: document.getElementById('tools-header'), - dividers: document.querySelectorAll('.section-divider'), - hideSections: document.querySelectorAll('.hide-section'), - shortcutsModal: document.getElementById('shortcuts-modal'), - closeShortcutsModalBtn: document.getElementById('close-shortcuts-modal'), - shortcutsList: document.getElementById('shortcuts-list'), - shortcutSearch: document.getElementById('shortcut-search'), - resetShortcutsBtn: document.getElementById('reset-shortcuts-btn'), - importShortcutsBtn: document.getElementById('import-shortcuts-btn'), - exportShortcutsBtn: document.getElementById('export-shortcuts-btn'), - openShortcutsBtn: document.getElementById('open-shortcuts-btn'), - warningModal: document.getElementById('warning-modal'), - warningTitle: document.getElementById('warning-title'), - warningMessage: document.getElementById('warning-message'), - warningCancelBtn: document.getElementById('warning-cancel-btn'), - warningConfirmBtn: document.getElementById('warning-confirm-btn'), + gridView: document.getElementById('grid-view'), + toolGrid: document.getElementById('tool-grid'), + toolInterface: document.getElementById('tool-interface'), + toolContent: document.getElementById('tool-content'), + backToGridBtn: document.getElementById('back-to-grid'), + loaderModal: document.getElementById('loader-modal'), + loaderText: document.getElementById('loader-text'), + alertModal: document.getElementById('alert-modal'), + alertTitle: document.getElementById('alert-title'), + alertMessage: document.getElementById('alert-message'), + alertOkBtn: document.getElementById('alert-ok'), + heroSection: document.getElementById('hero-section'), + featuresSection: document.getElementById('features-section'), + toolsHeader: document.getElementById('tools-header'), + dividers: document.querySelectorAll('.section-divider'), + hideSections: document.querySelectorAll('.hide-section'), + shortcutsModal: document.getElementById('shortcuts-modal'), + closeShortcutsModalBtn: document.getElementById('close-shortcuts-modal'), + shortcutsList: document.getElementById('shortcuts-list'), + shortcutSearch: document.getElementById('shortcut-search'), + resetShortcutsBtn: document.getElementById('reset-shortcuts-btn'), + importShortcutsBtn: document.getElementById('import-shortcuts-btn'), + exportShortcutsBtn: document.getElementById('export-shortcuts-btn'), + openShortcutsBtn: document.getElementById('open-shortcuts-btn'), + warningModal: document.getElementById('warning-modal'), + warningTitle: document.getElementById('warning-title'), + warningMessage: document.getElementById('warning-message'), + warningCancelBtn: document.getElementById('warning-cancel-btn'), + warningConfirmBtn: document.getElementById('warning-confirm-btn'), }; export const showLoader = (text = t('common.loading'), progress?: number) => { - if (dom.loaderText) dom.loaderText.textContent = text; + if (dom.loaderText) dom.loaderText.textContent = text; - // Add or update progress bar if progress is provided - const loaderModal = dom.loaderModal; - if (loaderModal) { - let progressBar = loaderModal.querySelector('.loader-progress-bar') as HTMLElement; - let progressContainer = loaderModal.querySelector('.loader-progress-container') as HTMLElement; + // Add or update progress bar if progress is provided + const loaderModal = dom.loaderModal; + if (loaderModal) { + let progressBar = loaderModal.querySelector( + '.loader-progress-bar' + ) as HTMLElement; + let progressContainer = loaderModal.querySelector( + '.loader-progress-container' + ) as HTMLElement; - if (progress !== undefined && progress >= 0) { - // Create progress container if it doesn't exist - if (!progressContainer) { - progressContainer = document.createElement('div'); - progressContainer.className = 'loader-progress-container w-64 mt-4'; - progressContainer.innerHTML = ` + if (progress !== undefined && progress >= 0) { + // Create progress container if it doesn't exist + if (!progressContainer) { + progressContainer = document.createElement('div'); + progressContainer.className = 'loader-progress-container w-64 mt-4'; + progressContainer.innerHTML = `

0%

`; - loaderModal.querySelector('.bg-gray-800')?.appendChild(progressContainer); - progressBar = progressContainer.querySelector('.loader-progress-bar') as HTMLElement; - } + loaderModal + .querySelector('.bg-gray-800') + ?.appendChild(progressContainer); + progressBar = progressContainer.querySelector( + '.loader-progress-bar' + ) as HTMLElement; + } - // Update progress - if (progressBar) { - progressBar.style.width = `${progress}%`; - } - const progressText = progressContainer.querySelector('.loader-progress-text'); - if (progressText) { - progressText.textContent = `${Math.round(progress)}%`; - } - progressContainer.classList.remove('hidden'); - } else { - // Hide progress bar if no progress provided - if (progressContainer) { - progressContainer.classList.add('hidden'); - } - } - - loaderModal.classList.remove('hidden'); + // Update progress + if (progressBar) { + progressBar.style.width = `${progress}%`; + } + const progressText = progressContainer.querySelector( + '.loader-progress-text' + ); + if (progressText) { + progressText.textContent = `${Math.round(progress)}%`; + } + progressContainer.classList.remove('hidden'); + } else { + // Hide progress bar if no progress provided + if (progressContainer) { + progressContainer.classList.add('hidden'); + } } + + loaderModal.classList.remove('hidden'); + } }; export const hideLoader = () => { - if (dom.loaderModal) dom.loaderModal.classList.add('hidden'); + if (dom.loaderModal) dom.loaderModal.classList.add('hidden'); }; -export const showAlert = (title: any, message: any, type: string = 'error', callback?: () => void) => { - if (dom.alertTitle) dom.alertTitle.textContent = title; - if (dom.alertMessage) dom.alertMessage.textContent = message; - if (dom.alertModal) dom.alertModal.classList.remove('hidden'); +export const showAlert = ( + title: any, + message: any, + type: string = 'error', + callback?: () => void +) => { + if (dom.alertTitle) dom.alertTitle.textContent = title; + if (dom.alertMessage) dom.alertMessage.textContent = message; + if (dom.alertModal) dom.alertModal.classList.remove('hidden'); - if (dom.alertOkBtn) { - const newOkBtn = dom.alertOkBtn.cloneNode(true) as HTMLElement; - dom.alertOkBtn.replaceWith(newOkBtn); - dom.alertOkBtn = newOkBtn; + if (dom.alertOkBtn) { + const newOkBtn = dom.alertOkBtn.cloneNode(true) as HTMLElement; + dom.alertOkBtn.replaceWith(newOkBtn); + dom.alertOkBtn = newOkBtn; - newOkBtn.addEventListener('click', () => { - hideAlert(); - if (callback) callback(); - }); - } + newOkBtn.addEventListener('click', () => { + hideAlert(); + if (callback) callback(); + }); + } }; export const hideAlert = () => { - if (dom.alertModal) dom.alertModal.classList.add('hidden'); + if (dom.alertModal) dom.alertModal.classList.add('hidden'); }; export const switchView = (view: any) => { - if (view === 'grid') { - dom.gridView.classList.remove('hidden'); - dom.toolInterface.classList.add('hidden'); - // show hero and features and header - dom.heroSection.classList.remove('hidden'); - dom.featuresSection.classList.remove('hidden'); - dom.toolsHeader.classList.remove('hidden'); - // show dividers - dom.dividers.forEach((divider) => { - divider.classList.remove('hidden'); - }); - // show hideSections - dom.hideSections.forEach((section) => { - section.classList.remove('hidden'); - }); + if (view === 'grid') { + dom.gridView.classList.remove('hidden'); + dom.toolInterface.classList.add('hidden'); + // show hero and features and header + dom.heroSection.classList.remove('hidden'); + dom.featuresSection.classList.remove('hidden'); + dom.toolsHeader.classList.remove('hidden'); + // show dividers + dom.dividers.forEach((divider) => { + divider.classList.remove('hidden'); + }); + // show hideSections + dom.hideSections.forEach((section) => { + section.classList.remove('hidden'); + }); - resetState(); - } else { - dom.gridView.classList.add('hidden'); - dom.toolInterface.classList.remove('hidden'); - dom.featuresSection.classList.add('hidden'); - dom.heroSection.classList.add('hidden'); - dom.toolsHeader.classList.add('hidden'); - dom.dividers.forEach((divider) => { - divider.classList.add('hidden'); - }); - dom.hideSections.forEach((section) => { - section.classList.add('hidden'); - }); - } + resetState(); + } else { + dom.gridView.classList.add('hidden'); + dom.toolInterface.classList.remove('hidden'); + dom.featuresSection.classList.add('hidden'); + dom.heroSection.classList.add('hidden'); + dom.toolsHeader.classList.add('hidden'); + dom.dividers.forEach((divider) => { + divider.classList.add('hidden'); + }); + dom.hideSections.forEach((section) => { + section.classList.add('hidden'); + }); + } }; const thumbnailState = { - sortableInstances: {}, + sortableInstances: {}, }; function initializeOrganizeSortable(containerId: any) { - const container = document.getElementById(containerId); - if (!container) return; + const container = document.getElementById(containerId); + if (!container) return; - if (thumbnailState.sortableInstances[containerId]) { - thumbnailState.sortableInstances[containerId].destroy(); - } + if (thumbnailState.sortableInstances[containerId]) { + thumbnailState.sortableInstances[containerId].destroy(); + } - thumbnailState.sortableInstances[containerId] = Sortable.create(container, { - animation: 150, - ghostClass: 'sortable-ghost', - chosenClass: 'sortable-chosen', - dragClass: 'sortable-drag', - filter: '.delete-page-btn', - preventOnFilter: true, - onStart: function (evt: any) { - evt.item.style.opacity = '0.5'; - }, - onEnd: function (evt: any) { - evt.item.style.opacity = '1'; - }, - }); + thumbnailState.sortableInstances[containerId] = Sortable.create(container, { + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + filter: '.delete-page-btn', + preventOnFilter: true, + onStart: function (evt: any) { + evt.item.style.opacity = '0.5'; + }, + onEnd: function (evt: any) { + evt.item.style.opacity = '1'; + }, + }); } /** @@ -180,243 +204,247 @@ function initializeOrganizeSortable(containerId: any) { * @param {object} pdfDoc The loaded pdf-lib document instance. */ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { - const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator'; - const container = document.getElementById(containerId); - if (!container) return; + const containerId = + toolId === 'organize' + ? 'page-organizer' + : toolId === 'delete-pages' + ? 'delete-pages-preview' + : 'page-rotator'; + const container = document.getElementById(containerId); + if (!container) return; - container.innerHTML = ''; + container.innerHTML = ''; - // Cleanup any previous lazy loading observers - cleanupLazyRendering(); + // Cleanup any previous lazy loading observers + cleanupLazyRendering(); - const currentRenderId = Date.now(); - container.dataset.renderId = currentRenderId.toString(); + const currentRenderId = Date.now(); + container.dataset.renderId = currentRenderId.toString(); - showLoader(t('multiTool.renderingTitle')); + showLoader(t('multiTool.renderingTitle')); - const pdfData = await pdfDoc.save(); - const pdf = await getPDFDocument({ data: pdfData }).promise; + const pdfData = await pdfDoc.save(); + const pdf = await getPDFDocument({ data: pdfData }).promise; - // Function to create wrapper element for each page - const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { - const wrapper = document.createElement('div'); - // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. - wrapper.dataset.pageIndex = pageNumber - 1; + // Function to create wrapper element for each page + const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { + const wrapper = document.createElement('div'); + // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. + wrapper.dataset.pageIndex = pageNumber - 1; - const imgContainer = document.createElement('div'); - imgContainer.className = - 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600'; + const imgContainer = document.createElement('div'); + imgContainer.className = 'relative'; - const img = document.createElement('img'); - img.src = canvas.toDataURL(); - img.className = 'max-w-full max-h-full object-contain'; + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'rounded-md shadow-md max-w-full h-auto'; - imgContainer.appendChild(img); + imgContainer.appendChild(img); - if (toolId === 'organize') { - wrapper.className = 'page-thumbnail relative group'; - wrapper.appendChild(imgContainer); + const pageNumSpan = document.createElement('div'); + pageNumSpan.className = + 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none'; + pageNumSpan.textContent = pageNumber.toString(); - const pageNumSpan = document.createElement('span'); - pageNumSpan.className = - 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; - pageNumSpan.textContent = pageNumber.toString(); + if (toolId === 'organize') { + 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 group'; - const deleteBtn = document.createElement('button'); - deleteBtn.className = - 'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', (e) => { - (e.currentTarget as HTMLElement).parentElement.remove(); + imgContainer.appendChild(pageNumSpan); + wrapper.appendChild(imgContainer); - // Renumber remaining pages - const pages = container.querySelectorAll('.page-thumbnail'); - pages.forEach((page, index) => { - const numSpan = page.querySelector('span'); - if (numSpan) { - numSpan.textContent = (index + 1).toString(); - } - }); + const deleteBtn = document.createElement('button'); + deleteBtn.className = + 'delete-page-btn absolute top-1 right-1 bg-red-600 hover:bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center z-10'; + deleteBtn.innerHTML = '×'; + deleteBtn.addEventListener('click', (e) => { + (e.currentTarget as HTMLElement).parentElement.remove(); - initializeOrganizeSortable(containerId); - }); + // Renumber remaining pages + const pages = container.querySelectorAll('.page-thumbnail'); + pages.forEach((page, index) => { + const numSpan = page.querySelector('.bg-indigo-600'); + if (numSpan) { + numSpan.textContent = (index + 1).toString(); + } + }); - wrapper.append(pageNumSpan, deleteBtn); - } else if (toolId === 'rotate') { - wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group'; + initializeOrganizeSortable(containerId); + }); - // Read rotation from state (handles "Rotate All" on lazy-loaded pages) - const rotationStateArray = getRotationState(); - const pageIndex = pageNumber - 1; - const initialRotation = rotationStateArray[pageIndex] || 0; + wrapper.appendChild(deleteBtn); + } else if (toolId === 'rotate') { + wrapper.className = + 'page-rotator-item flex flex-col items-center gap-2 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors relative group'; - wrapper.dataset.rotation = initialRotation.toString(); - img.classList.add('transition-transform', 'duration-300'); + // Read rotation from state (handles "Rotate All" on lazy-loaded pages) + const rotationStateArray = getRotationState(); + const pageIndex = pageNumber - 1; + const initialRotation = rotationStateArray[pageIndex] || 0; - // Apply initial rotation if any - if (initialRotation !== 0) { - img.style.transform = `rotate(${initialRotation}deg)`; - } + wrapper.dataset.rotation = initialRotation.toString(); + img.classList.add('transition-transform', 'duration-300'); - wrapper.appendChild(imgContainer); + // Apply initial rotation if any + if (initialRotation !== 0) { + img.style.transform = `rotate(${initialRotation}deg)`; + } - // Page Number Overlay (Top Left) - const pageNumSpan = document.createElement('span'); - pageNumSpan.className = - 'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none'; - pageNumSpan.textContent = pageNumber.toString(); - wrapper.appendChild(pageNumSpan); + imgContainer.appendChild(pageNumSpan); + wrapper.appendChild(imgContainer); - const controlsDiv = document.createElement('div'); - controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1'; + const controlsDiv = document.createElement('div'); + controlsDiv.className = + 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1'; - // Custom Stepper Component - const stepperContainer = document.createElement('div'); - stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8'; + // Custom Stepper Component + const stepperContainer = document.createElement('div'); + stepperContainer.className = + 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8'; - const decrementBtn = document.createElement('button'); - decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center'; - decrementBtn.innerHTML = ''; + const decrementBtn = document.createElement('button'); + decrementBtn.className = + 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center'; + decrementBtn.innerHTML = ''; - const angleInput = document.createElement('input'); - angleInput.type = 'number'; - angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none'; - angleInput.value = initialRotation.toString(); - angleInput.placeholder = "0"; + const angleInput = document.createElement('input'); + angleInput.type = 'number'; + angleInput.className = + 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none'; + angleInput.value = initialRotation.toString(); + angleInput.placeholder = '0'; - const incrementBtn = document.createElement('button'); - incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center'; - incrementBtn.innerHTML = ''; + const incrementBtn = document.createElement('button'); + incrementBtn.className = + 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center'; + incrementBtn.innerHTML = ''; - // Helper to update rotation - const updateRotation = (newRotation: number) => { - const card = wrapper; // Closure capture - const imgEl = card.querySelector('img'); - const pageIndex = pageNumber - 1; + // Helper to update rotation + const updateRotation = (newRotation: number) => { + const card = wrapper; // Closure capture + const imgEl = card.querySelector('img'); + const pageIndex = pageNumber - 1; - // Update UI - angleInput.value = newRotation.toString(); - card.dataset.rotation = newRotation.toString(); - imgEl.style.transform = `rotate(${newRotation}deg)`; + // Update UI + angleInput.value = newRotation.toString(); + card.dataset.rotation = newRotation.toString(); + imgEl.style.transform = `rotate(${newRotation}deg)`; - // Update State - updateRotationState(pageIndex, newRotation); - }; + // Update State + updateRotationState(pageIndex, newRotation); + }; - // Event Listeners - decrementBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const current = parseInt(angleInput.value) || 0; - updateRotation(current - 1); - }); + // Event Listeners + decrementBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const current = parseInt(angleInput.value) || 0; + updateRotation(current - 1); + }); - incrementBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const current = parseInt(angleInput.value) || 0; - updateRotation(current + 1); - }); + incrementBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const current = parseInt(angleInput.value) || 0; + updateRotation(current + 1); + }); - angleInput.addEventListener('change', (e) => { - e.stopPropagation(); - const val = parseInt((e.target as HTMLInputElement).value) || 0; - updateRotation(val); - }); - angleInput.addEventListener('click', (e) => e.stopPropagation()); + angleInput.addEventListener('change', (e) => { + e.stopPropagation(); + const val = parseInt((e.target as HTMLInputElement).value) || 0; + updateRotation(val); + }); + angleInput.addEventListener('click', (e) => e.stopPropagation()); - stepperContainer.append(decrementBtn, angleInput, incrementBtn); + stepperContainer.append(decrementBtn, angleInput, incrementBtn); - const rotateBtn = document.createElement('button'); - rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0'; - rotateBtn.title = 'Rotate +90°'; - rotateBtn.innerHTML = ''; - rotateBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const current = parseInt(angleInput.value) || 0; - updateRotation(current + 90); - }); + const rotateBtn = document.createElement('button'); + rotateBtn.className = + 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0'; + rotateBtn.title = 'Rotate +90°'; + rotateBtn.innerHTML = ''; + rotateBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const current = parseInt(angleInput.value) || 0; + updateRotation(current + 90); + }); - controlsDiv.append(stepperContainer, rotateBtn); - wrapper.appendChild(controlsDiv); - } else if (toolId === 'delete-pages') { - wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200'; - wrapper.dataset.pageNumber = pageNumber.toString(); + controlsDiv.append(stepperContainer, rotateBtn); + wrapper.appendChild(controlsDiv); + } else if (toolId === 'delete-pages') { + wrapper.className = + 'page-thumbnail relative cursor-pointer 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 group'; + wrapper.dataset.pageNumber = pageNumber.toString(); - const innerContainer = document.createElement('div'); - innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200'; - innerContainer.appendChild(img); - wrapper.appendChild(innerContainer); + imgContainer.appendChild(pageNumSpan); + wrapper.appendChild(imgContainer); - const pageNumSpan = document.createElement('span'); - pageNumSpan.className = - 'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none'; - pageNumSpan.textContent = pageNumber.toString(); - wrapper.appendChild(pageNumSpan); + wrapper.addEventListener('click', () => { + const input = document.getElementById( + 'pages-to-delete' + ) as HTMLInputElement; + if (!input) return; - wrapper.addEventListener('click', () => { - const input = document.getElementById('pages-to-delete') as HTMLInputElement; - if (!input) return; + const currentVal = input.value; + let pages = currentVal + .split(',') + .map((s) => s.trim()) + .filter((s) => s); + const pageStr = pageNumber.toString(); - const currentVal = input.value; - let pages = currentVal.split(',').map(s => s.trim()).filter(s => s); - const pageStr = pageNumber.toString(); - - if (pages.includes(pageStr)) { - pages = pages.filter(p => p !== pageStr); - } else { - pages.push(pageStr); - } - - pages.sort((a, b) => { - const numA = parseInt(a.split('-')[0]); - const numB = parseInt(b.split('-')[0]); - return numA - numB; - }); - - input.value = pages.join(', '); - - input.dispatchEvent(new Event('input')); - }); + if (pages.includes(pageStr)) { + pages = pages.filter((p) => p !== pageStr); + } else { + pages.push(pageStr); } - return wrapper; - }; + pages.sort((a, b) => { + const numA = parseInt(a.split('-')[0]); + const numB = parseInt(b.split('-')[0]); + return numA - numB; + }); - try { - // Render pages progressively with lazy loading - await renderPagesProgressively( - pdf, - container, - createWrapper, - { - batchSize: 8, - useLazyLoading: true, - lazyLoadMargin: '300px', - onProgress: (current, total) => { - showLoader(`Rendering page previews: ${current}/${total}`); - }, - onBatchComplete: () => { - createIcons({ icons }); - }, - shouldCancel: () => { - return container.dataset.renderId !== currentRenderId.toString(); - } - } - ); + input.value = pages.join(', '); - if (toolId === 'organize') { - initializeOrganizeSortable(containerId); - } else if (toolId === 'delete-pages') { - // No sortable needed for delete pages - } - - // Reinitialize lucide icons for dynamically added elements - createIcons({ icons }); - } catch (error) { - console.error('Error rendering page thumbnails:', error); - showAlert(t('multiTool.error'), t('multiTool.errorRendering')); - } finally { - hideLoader(); + input.dispatchEvent(new Event('input')); + }); } + + return wrapper; + }; + + try { + // Render pages progressively with lazy loading + await renderPagesProgressively(pdf, container, createWrapper, { + batchSize: 8, + useLazyLoading: true, + lazyLoadMargin: '300px', + onProgress: (current, total) => { + showLoader(`Rendering page previews: ${current}/${total}`); + }, + onBatchComplete: () => { + createIcons({ icons }); + }, + shouldCancel: () => { + return container.dataset.renderId !== currentRenderId.toString(); + }, + }); + + if (toolId === 'organize') { + initializeOrganizeSortable(containerId); + } else if (toolId === 'delete-pages') { + // No sortable needed for delete pages + } + + // Reinitialize lucide icons for dynamically added elements + createIcons({ icons }); + + // Attach Quick Look page preview + initPagePreview(container, pdf); + } catch (error) { + console.error('Error rendering page thumbnails:', error); + showAlert(t('multiTool.error'), t('multiTool.errorRendering')); + } finally { + hideLoader(); + } }; /** @@ -425,36 +453,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { * @param {File[]} files The array of file objects. */ export const renderFileDisplay = (container: any, files: any) => { - container.textContent = ''; - if (files.length > 0) { - files.forEach((file: any) => { - const fileDiv = document.createElement('div'); - fileDiv.className = - 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + container.textContent = ''; + if (files.length > 0) { + files.forEach((file: any) => { + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const nameSpan = document.createElement('span'); - nameSpan.className = 'truncate font-medium text-gray-200'; - nameSpan.textContent = file.name; + const nameSpan = document.createElement('span'); + nameSpan.className = 'truncate font-medium text-gray-200'; + nameSpan.textContent = file.name; - const sizeSpan = document.createElement('span'); - sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'; - sizeSpan.textContent = formatBytes(file.size); + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'; + sizeSpan.textContent = formatBytes(file.size); - fileDiv.append(nameSpan, sizeSpan); - container.appendChild(fileDiv); - }); - } + fileDiv.append(nameSpan, sizeSpan); + container.appendChild(fileDiv); + }); + } }; const createFileInputHTML = (options = {}) => { - // @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'. - const multiple = options.multiple ? 'multiple' : ''; - // @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'. - const acceptedFiles = options.accept || 'application/pdf'; - // @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message - const showControls = options.showControls || false; // NEW: Add this parameter + // @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'. + const multiple = options.multiple ? 'multiple' : ''; + // @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'. + const acceptedFiles = options.accept || 'application/pdf'; + // @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message + const showControls = options.showControls || false; // NEW: Add this parameter - return ` + return `
@@ -465,7 +493,8 @@ const createFileInputHTML = (options = {}) => {
- ${showControls + ${ + showControls ? ` -