diff --git a/public/workers/alternate-merge.worker.d.ts b/public/workers/alternate-merge.worker.d.ts new file mode 100644 index 0000000..d25e003 --- /dev/null +++ b/public/workers/alternate-merge.worker.d.ts @@ -0,0 +1,23 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface InterleaveFile { + name: string; + data: ArrayBuffer; +} + +interface InterleaveMessage { + command: 'interleave'; + files: InterleaveFile[]; +} + +interface InterleaveSuccessResponse { + status: 'success'; + pdfBytes: ArrayBuffer; +} + +interface InterleaveErrorResponse { + status: 'error'; + message: string; +} + +type InterleaveResponse = InterleaveSuccessResponse | InterleaveErrorResponse; diff --git a/public/workers/merge.worker.d.ts b/public/workers/merge.worker.d.ts new file mode 100644 index 0000000..8e20242 --- /dev/null +++ b/public/workers/merge.worker.d.ts @@ -0,0 +1,33 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface MergeJob { + fileName: string; + rangeType: 'all' | 'specific' | 'single' | 'range'; + rangeString?: string; + pageIndex?: number; + startPage?: number; + endPage?: number; +} + +interface MergeFile { + name: string; + data: ArrayBuffer; +} + +interface MergeMessage { + command: 'merge'; + files: MergeFile[]; + jobs: MergeJob[]; +} + +interface MergeSuccessResponse { + status: 'success'; + pdfBytes: ArrayBuffer; +} + +interface MergeErrorResponse { + status: 'error'; + message: string; +} + +type MergeResponse = MergeSuccessResponse | MergeErrorResponse; diff --git a/src/css/styles.css b/src/css/styles.css index baaf42b..f8de956 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -600,4 +600,16 @@ button:disabled, mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%); -webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%); pointer-events: none; +} + +/* Hide spin buttons for number inputs */ +input[type="number"] { + appearance: none; + -moz-appearance: textfield; /* Firefox */ +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; /* Chrome, Safari, Edge */ + margin: 0; } \ No newline at end of file diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index ad7fff8..445a1aa 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -117,7 +117,7 @@ async function handleSinglePdfUpload(toolId, file) { .toString(); } - if (toolId === 'organize' || toolId === 'rotate') { + if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') { await renderPageThumbnails(toolId, state.pdfDoc); if (toolId === 'rotate') { @@ -134,14 +134,19 @@ async function handleSinglePdfUpload(toolId, file) { const rotateAllRightBtn = document.getElementById( 'rotate-all-right-btn' ); + const rotateAllCustomBtn = document.getElementById('rotate-all-custom-btn'); + const rotateAllCustomInput = document.getElementById('custom-rotate-all-input') as HTMLInputElement; + const rotateAllDecrementBtn = document.getElementById('rotate-all-decrement-btn'); + const rotateAllIncrementBtn = document.getElementById('rotate-all-increment-btn'); + rotateAllControls.classList.remove('hidden'); createIcons({ icons }); - const rotateAll = (direction) => { + const rotateAll = (angle: number) => { // Update rotation state for ALL pages (including unrendered ones) for (let i = 0; i < rotationState.length; i++) { - rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360; + rotationState[i] = (rotationState[i] + angle); } // Update DOM for currently rendered pages @@ -149,15 +154,44 @@ async function handleSinglePdfUpload(toolId, file) { const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0'); const newRotation = rotationState[pageIndex]; (item as HTMLElement).dataset.rotation = newRotation.toString(); + const thumbnail = item.querySelector('canvas, img'); if (thumbnail) { (thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`; } + + const input = item.querySelector('input'); + if (input) { + input.value = newRotation.toString(); + } }); }; - rotateAllLeftBtn.onclick = () => rotateAll(-1); - rotateAllRightBtn.onclick = () => rotateAll(1); + rotateAllLeftBtn.onclick = () => rotateAll(-90); + rotateAllRightBtn.onclick = () => rotateAll(90); + + if (rotateAllCustomBtn && rotateAllCustomInput) { + rotateAllCustomBtn.onclick = () => { + const angle = parseInt(rotateAllCustomInput.value); + if (!isNaN(angle) && angle !== 0) { + rotateAll(angle); + } + }; + + if (rotateAllDecrementBtn) { + rotateAllDecrementBtn.onclick = () => { + let current = parseInt(rotateAllCustomInput.value) || 0; + rotateAllCustomInput.value = (current - 1).toString(); + }; + } + + if (rotateAllIncrementBtn) { + rotateAllIncrementBtn.onclick = () => { + let current = parseInt(rotateAllCustomInput.value) || 0; + rotateAllCustomInput.value = (current + 1).toString(); + }; + } + } } } diff --git a/src/js/logic/alternate-merge.ts b/src/js/logic/alternate-merge.ts index 950c28b..be25ca3 100644 --- a/src/js/logic/alternate-merge.ts +++ b/src/js/logic/alternate-merge.ts @@ -3,9 +3,14 @@ import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/he import { state } from '../state.js'; import Sortable from 'sortablejs'; -const alternateMergeState = { - pdfDocs: {} as Record, - pdfBytes: {} as Record, +interface AlternateMergeState { + pdfDocs: Record; + pdfBytes: Record; +} + +const alternateMergeState: AlternateMergeState = { + pdfDocs: {}, + pdfBytes: {}, }; const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js'); @@ -98,7 +103,7 @@ export async function alternateMerge() { (li) => (li as HTMLElement).dataset.fileName ).filter(Boolean) as string[]; - const filesToMerge: { name: string; data: ArrayBuffer }[] = []; + const filesToMerge: InterleaveFile[] = []; for (const name of sortedFileNames) { const bytes = alternateMergeState.pdfBytes[name]; if (bytes) { @@ -112,12 +117,14 @@ export async function alternateMerge() { return; } - alternateMergeWorker.postMessage({ + const message: InterleaveMessage = { command: 'interleave', files: filesToMerge - }, filesToMerge.map(f => f.data)); + }; - alternateMergeWorker.onmessage = (e) => { + alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data)); + + alternateMergeWorker.onmessage = (e: MessageEvent) => { hideLoader(); if (e.data.status === 'success') { const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' }); diff --git a/src/js/logic/delete-pages.ts b/src/js/logic/delete-pages.ts index 6c4b280..c8aa86a 100644 --- a/src/js/logic/delete-pages.ts +++ b/src/js/logic/delete-pages.ts @@ -68,3 +68,44 @@ export async function deletePages() { hideLoader(); } } + +export function setupDeletePagesTool() { + const input = document.getElementById('pages-to-delete') as HTMLInputElement; + if (!input) return; + + const updateHighlights = () => { + const val = input.value; + const pagesToDelete = new Set(); + + const parts = val.split(','); + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed.includes('-')) { + const [start, end] = trimmed.split('-').map(Number); + if (!isNaN(start) && !isNaN(end) && start <= end) { + for (let i = start; i <= end; i++) pagesToDelete.add(i); + } + } else { + const num = Number(trimmed); + if (!isNaN(num)) pagesToDelete.add(num); + } + } + + const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail'); + thumbnails.forEach((thumb) => { + const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0'); + const innerContainer = thumb.querySelector('div.relative'); + + if (pagesToDelete.has(pageNum)) { + innerContainer?.classList.add('border-red-500'); + innerContainer?.classList.remove('border-gray-600'); + } else { + innerContainer?.classList.remove('border-red-500'); + innerContainer?.classList.add('border-gray-600'); + } + }); + }; + + input.addEventListener('input', updateHighlights); + updateHighlights(); +} diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index a6b96da..ecf1eff 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -18,7 +18,7 @@ import { pdfToPng } from './pdf-to-png.js'; import { pngToPdf } from './png-to-pdf.js'; import { pdfToWebp } from './pdf-to-webp.js'; import { webpToPdf } from './webp-to-pdf.js'; -import { deletePages } from './delete-pages.js'; +import { deletePages, setupDeletePagesTool } from './delete-pages.js'; import { addBlankPage } from './add-blank-page.js'; import { extractPages } from './extract-pages.js'; import { addWatermark, setupWatermarkUI } from './add-watermark.js'; @@ -92,7 +92,7 @@ export const toolLogic = { 'png-to-pdf': pngToPdf, 'pdf-to-webp': pdfToWebp, 'webp-to-pdf': webpToPdf, - 'delete-pages': deletePages, + 'delete-pages': { process: deletePages, setup: setupDeletePagesTool }, 'add-blank-page': addBlankPage, 'extract-pages': extractPages, 'add-watermark': { process: addWatermark, setup: setupWatermarkUI }, diff --git a/src/js/logic/merge.ts b/src/js/logic/merge.ts index 756b942..3024876 100644 --- a/src/js/logic/merge.ts +++ b/src/js/logic/merge.ts @@ -1,7 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts'; import { state } from '../state.ts'; -import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts'; +import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; @@ -9,9 +9,22 @@ import Sortable from 'sortablejs'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); -const mergeState = { - pdfDocs: {} as Record, - pdfBytes: {} as Record, +interface MergeState { + pdfDocs: Record; + pdfBytes: Record; + activeMode: 'file' | 'page'; + sortableInstances: { + fileList?: Sortable; + pageThumbnails?: Sortable; + }; + isRendering: boolean; + cachedThumbnails: boolean | null; + lastFileHash: string | null; +} + +const mergeState: MergeState = { + pdfDocs: {}, + pdfBytes: {}, activeMode: 'file', sortableInstances: {}, isRendering: false, @@ -21,47 +34,14 @@ const mergeState = { const mergeWorker = new Worker('/workers/merge.worker.js'); -function parsePageRanges(rangeString: any, totalPages: any) { - const indices = new Set(); - if (!rangeString.trim()) return []; - - const ranges = rangeString.split(','); - for (const range of ranges) { - const trimmedRange = range.trim(); - if (trimmedRange.includes('-')) { - const [start, end] = trimmedRange.split('-').map(Number); - if ( - isNaN(start) || - isNaN(end) || - start < 1 || - end > totalPages || - start > end - ) - continue; - for (let i = start; i <= end; i++) { - indices.add(i - 1); - } - } else { - const pageNum = Number(trimmedRange); - if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue; - indices.add(pageNum - 1); - } - } - // @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message - return Array.from(indices).sort((a, b) => a - b); -} - function initializeFileListSortable() { const fileList = document.getElementById('file-list'); if (!fileList) return; - // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'. if (mergeState.sortableInstances.fileList) { - // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'. mergeState.sortableInstances.fileList.destroy(); } - // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'. mergeState.sortableInstances.fileList = Sortable.create(fileList, { handle: '.drag-handle', animation: 150, @@ -81,13 +61,10 @@ function initializePageThumbnailsSortable() { const container = document.getElementById('page-merge-preview'); if (!container) return; - // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message if (mergeState.sortableInstances.pageThumbnails) { - // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message mergeState.sortableInstances.pageThumbnails.destroy(); } - // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message mergeState.sortableInstances.pageThumbnails = Sortable.create(container, { animation: 150, ghostClass: 'sortable-ghost', @@ -224,8 +201,8 @@ async function renderPageMergeThumbnails() { export async function merge() { showLoader('Merging PDFs...'); try { - const jobs: any[] = []; - const filesToMerge: any[] = []; + const jobs: MergeJob[] = []; + const filesToMerge: MergeFile[] = []; const uniqueFileNames = new Set(); if (mergeState.activeMode === 'file') { @@ -234,8 +211,7 @@ export async function merge() { const sortedFiles = Array.from(fileList.children) .map((li) => { - // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... - return state.files.find((f) => f.name === li.dataset.fileName); + return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName); }) .filter(Boolean); @@ -267,10 +243,9 @@ export async function merge() { const rawPages: { fileName: string; pageIndex: number }[] = []; for (const el of pageElements) { - // @ts-expect-error TS(2339) - const fileName = el.dataset.fileName; - // @ts-expect-error TS(2339) - const pageIndex = parseInt(el.dataset.pageIndex, 10); // 0-based index from dataset + const element = el as HTMLElement; + const fileName = element.dataset.fileName; + const pageIndex = parseInt(element.dataset.pageIndex || '', 10); // 0-based index from dataset if (fileName && !isNaN(pageIndex)) { uniqueFileNames.add(fileName); @@ -304,8 +279,8 @@ export async function merge() { jobs.push({ fileName: current.fileName, rangeType: 'range', - startPage: current.pageIndex + 1, - endPage: endPage + 1 + startPage: current.pageIndex + 1, + endPage: endPage + 1 }); } } @@ -324,13 +299,15 @@ export async function merge() { } } - mergeWorker.postMessage({ + const message: MergeMessage = { command: 'merge', files: filesToMerge, jobs: jobs - }, filesToMerge.map(f => f.data)); + }; - mergeWorker.onmessage = (e) => { + mergeWorker.postMessage(message, filesToMerge.map(f => f.data)); + + mergeWorker.onmessage = (e: MessageEvent) => { hideLoader(); if (e.data.status === 'success') { const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' }); diff --git a/src/js/logic/rotate.ts b/src/js/logic/rotate.ts index 623bf10..19caa13 100644 --- a/src/js/logic/rotate.ts +++ b/src/js/logic/rotate.ts @@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js'; import { state } from '../state.js'; import { getRotationState, resetRotationState } from '../handlers/fileHandler.js'; -import { degrees } from 'pdf-lib'; +import { PDFDocument, degrees } from 'pdf-lib'; export async function rotate() { showLoader('Applying rotations...'); try { - const pages = state.pdfDoc.getPages(); + const originalPdf = state.pdfDoc; + const pageCount = originalPdf.getPageCount(); const rotationStateArray = getRotationState(); - // Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated - rotationStateArray.forEach((rotation, pageIndex) => { - if (rotation !== 0 && pages[pageIndex]) { - const currentRotation = pages[pageIndex].getRotation().angle; - pages[pageIndex].setRotation(degrees(currentRotation + rotation)); - } - }); + const newPdfDoc = await PDFDocument.create(); - const rotatedPdfBytes = await state.pdfDoc.save(); + for (let i = 0; i < pageCount; i++) { + const rotation = rotationStateArray[i] || 0; + const originalPage = originalPdf.getPage(i); + const currentRotation = originalPage.getRotation().angle; + const totalRotation = currentRotation + rotation; + + if (totalRotation % 90 === 0) { + const [copiedPage] = await newPdfDoc.copyPages(originalPdf, [i]); + copiedPage.setRotation(degrees(totalRotation)); + newPdfDoc.addPage(copiedPage); + } else { + const embeddedPage = await newPdfDoc.embedPage(originalPage); + const { width, height } = embeddedPage.scale(1); + + const angleRad = (totalRotation * Math.PI) / 180; + const absCos = Math.abs(Math.cos(angleRad)); + const absSin = Math.abs(Math.sin(angleRad)); + + const newWidth = width * absCos + height * absSin; + const newHeight = width * absSin + height * absCos; + + const newPage = newPdfDoc.addPage([newWidth, newHeight]); + + const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad)); + const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad)); + + newPage.drawPage(embeddedPage, { + x, + y, + width, + height, + rotate: degrees(totalRotation), + }); + } + } + + const rotatedPdfBytes = await newPdfDoc.save(); downloadFile( - new Blob([rotatedPdfBytes], { type: 'application/pdf' }), + new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }), 'rotated.pdf' ); diff --git a/src/js/ui.ts b/src/js/ui.ts index 287b788..e85d265 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -129,7 +129,7 @@ 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' : 'page-rotator'; + const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator'; const container = document.getElementById(containerId); if (!container) return; @@ -138,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { // Cleanup any previous lazy loading observers cleanupLazyRendering(); + const currentRenderId = Date.now(); + container.dataset.renderId = currentRenderId.toString(); + showLoader('Rendering page previews...'); const pdfData = await pdfDoc.save(); @@ -189,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { wrapper.append(pageNumSpan, deleteBtn); } else if (toolId === 'rotate') { - wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; + wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group'; // Read rotation from state (handles "Rotate All" on lazy-loaded pages) const rotationStateArray = getRotationState(); @@ -206,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { wrapper.appendChild(imgContainer); - const controlsDiv = document.createElement('div'); - controlsDiv.className = 'flex items-center justify-center gap-3 w-full'; - + // Page Number Overlay (Top Left) const pageNumSpan = document.createElement('span'); - pageNumSpan.className = 'font-medium text-sm text-white'; + 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); - const rotateBtn = document.createElement('button'); - rotateBtn.className = - 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full'; - rotateBtn.title = 'Rotate 90°'; - rotateBtn.innerHTML = ''; - rotateBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const card = (e.currentTarget as HTMLElement).closest( - '.page-rotator-item' - ) as HTMLElement; + 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'; + + 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 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; - let currentRotation = parseInt(card.dataset.rotation); - currentRotation = (currentRotation + 90) % 360; - card.dataset.rotation = currentRotation.toString(); - imgEl.style.transform = `rotate(${currentRotation}deg)`; - updateRotationState(pageIndex, currentRotation); + // Update UI + angleInput.value = newRotation.toString(); + card.dataset.rotation = newRotation.toString(); + imgEl.style.transform = `rotate(${newRotation}deg)`; + + // Update State + updateRotationState(pageIndex, newRotation); + }; + + // Event Listeners + decrementBtn.addEventListener('click', (e) => { + e.stopPropagation(); + let current = parseInt(angleInput.value) || 0; + updateRotation(current - 1); }); - controlsDiv.append(pageNumSpan, rotateBtn); + incrementBtn.addEventListener('click', (e) => { + e.stopPropagation(); + let current = parseInt(angleInput.value) || 0; + updateRotation(current + 1); + }); + + angleInput.addEventListener('change', (e) => { + e.stopPropagation(); + let val = parseInt((e.target as HTMLInputElement).value) || 0; + updateRotation(val); + }); + angleInput.addEventListener('click', (e) => e.stopPropagation()); + + 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(); + let 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(); + + 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); + + 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; + + 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')); + }); } return wrapper; @@ -255,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { }, 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 @@ -580,15 +675,49 @@ export const toolTemplates = { `,