import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts'; import { state } from '../state.ts'; import { PDFDocument as PDFLibDocument } 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(); const mergeState = { pdfDocs: {}, activeMode: 'file', sortableInstances: {}, isRendering: false, cachedThumbnails: null, lastFileHash: null, }; 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, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', onStart: function (evt: any) { evt.item.style.opacity = '0.5'; }, onEnd: function (evt: any) { evt.item.style.opacity = '1'; }, }); } 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', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', onStart: function (evt: any) { evt.item.style.opacity = '0.5'; }, onEnd: function (evt: any) { evt.item.style.opacity = '1'; }, }); } function generateFileHash() { return (state.files as File[]) .map((f) => `${f.name}-${f.size}-${f.lastModified}`) .join('|'); } async function renderPageMergeThumbnails() { const container = document.getElementById('page-merge-preview'); if (!container) return; const currentFileHash = generateFileHash(); const filesChanged = currentFileHash !== mergeState.lastFileHash; if (!filesChanged && mergeState.cachedThumbnails !== null) { // Simple check to see if it's already rendered to avoid flicker. if (container.firstChild) { initializePageThumbnailsSortable(); return; } } if (mergeState.isRendering) { return; } mergeState.isRendering = true; container.textContent = ''; let currentPageNumber = 0; let totalPages = state.files.reduce((sum, file) => { const pdfDoc = mergeState.pdfDocs[file.name]; return sum + (pdfDoc ? pdfDoc.getPageCount() : 0); }, 0); try { const thumbnailsHTML = []; for (const file of state.files) { const pdfDoc = mergeState.pdfDocs[file.name]; if (!pdfDoc) continue; const pdfData = await pdfDoc.save(); const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; for (let i = 1; i <= pdfjsDoc.numPages; i++) { currentPageNumber++; showLoader( `Rendering page previews: ${currentPageNumber}/${totalPages}` ); const page = await pdfjsDoc.getPage(i); const viewport = page.getViewport({ scale: 0.3 }); const canvas = document.createElement('canvas'); canvas.height = viewport.height; canvas.width = viewport.width; const context = canvas.getContext('2d')!; await page.render({ canvasContext: context, canvas: canvas, viewport, }).promise; const wrapper = document.createElement('div'); wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors'; wrapper.dataset.fileName = file.name; wrapper.dataset.pageIndex = (i - 1).toString(); const imgContainer = document.createElement('div'); imgContainer.className = 'relative'; const img = document.createElement('img'); img.src = canvas.toDataURL(); img.className = 'rounded-md shadow-md max-w-full h-auto'; 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'; pageNumDiv.textContent = i.toString(); imgContainer.append(img, pageNumDiv); const fileNamePara = document.createElement('p'); fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center'; const fullTitle = `${file.name} (page ${i})`; fileNamePara.title = fullTitle; fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`; wrapper.append(imgContainer, fileNamePara); container.appendChild(wrapper); } pdfjsDoc.destroy(); } mergeState.cachedThumbnails = true; mergeState.lastFileHash = currentFileHash; initializePageThumbnailsSortable(); } catch (error) { console.error('Error rendering page thumbnails:', error); showAlert('Error', 'Failed to render page thumbnails'); } finally { hideLoader(); mergeState.isRendering = false; } } export async function merge() { showLoader('Merging PDFs...'); try { const newPdfDoc = await PDFLibDocument.create(); if (mergeState.activeMode === 'file') { const fileList = document.getElementById('file-list'); const sortedFiles = Array.from(fileList.children) .map((li) => { // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message return state.files.find((f) => f.name === li.dataset.fileName); }) .filter(Boolean); for (const file of sortedFiles) { const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_'); const rangeInput = document.getElementById(`range-${safeFileName}`); if (!rangeInput) continue; // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message const rangeInputValue = rangeInput.value; const sourcePdf = mergeState.pdfDocs[file.name]; if (!sourcePdf) continue; const totalPages = sourcePdf.getPageCount(); const pageIndices = parsePageRanges(rangeInputValue, totalPages); const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices(); const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy); copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); } } else { const pageContainer = document.getElementById('page-merge-preview'); const pageElements = Array.from(pageContainer.children); for (const el of pageElements) { // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message const fileName = el.dataset.fileName; // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message const pageIndex = parseInt(el.dataset.pageIndex, 10); const sourcePdf = mergeState.pdfDocs[fileName]; if (sourcePdf && !isNaN(pageIndex)) { const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [ pageIndex, ]); newPdfDoc.addPage(copiedPage); } } } const mergedPdfBytes = await newPdfDoc.save(); downloadFile( new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf' ); showAlert('Success', 'PDFs merged successfully!'); } catch (e) { console.error('Merge error:', e); showAlert( 'Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.' ); } finally { hideLoader(); } } export async function setupMergeTool() { document.getElementById('merge-options').classList.remove('hidden'); // @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message document.getElementById('process-btn').disabled = false; const wasInPageMode = mergeState.activeMode === 'page'; showLoader('Loading PDF documents...'); try { for (const file of state.files) { if (!mergeState.pdfDocs[file.name]) { const pdfBytes = await readFileAsArrayBuffer(file); mergeState.pdfDocs[file.name] = await PDFLibDocument.load( pdfBytes as ArrayBuffer, { ignoreEncryption: true, } ); } } } catch (error) { console.error('Error loading PDFs:', error); showAlert('Error', 'Failed to load one or more PDF files'); return; } finally { hideLoader(); } const fileModeBtn = document.getElementById('file-mode-btn'); const pageModeBtn = document.getElementById('page-mode-btn'); const filePanel = document.getElementById('file-mode-panel'); const pagePanel = document.getElementById('page-mode-panel'); const fileList = document.getElementById('file-list'); fileList.textContent = ''; // Clear list safely (state.files as File[]).forEach((f) => { const doc = mergeState.pdfDocs[f.name]; const pageCount = doc ? doc.getPageCount() : 'N/A'; const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_'); const li = document.createElement('li'); li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'; li.dataset.fileName = f.name; const mainDiv = document.createElement('div'); mainDiv.className = 'flex items-center justify-between'; const nameSpan = document.createElement('span'); nameSpan.className = 'truncate font-medium text-white flex-1 mr-2'; nameSpan.title = f.name; nameSpan.textContent = f.name; const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; dragHandle.innerHTML = ``; // Safe: static content mainDiv.append(nameSpan, dragHandle); const rangeDiv = document.createElement('div'); rangeDiv.className = 'mt-2'; const label = document.createElement('label'); label.htmlFor = `range-${safeFileName}`; label.className = 'text-xs text-gray-400'; label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`; const input = document.createElement('input'); input.type = 'text'; input.id = `range-${safeFileName}`; input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors'; input.placeholder = 'Leave blank for all pages'; rangeDiv.append(label, input); li.append(mainDiv, rangeDiv); fileList.appendChild(li); }); initializeFileListSortable(); const newFileModeBtn = fileModeBtn.cloneNode(true); const newPageModeBtn = pageModeBtn.cloneNode(true); fileModeBtn.replaceWith(newFileModeBtn); pageModeBtn.replaceWith(newPageModeBtn); newFileModeBtn.addEventListener('click', () => { if (mergeState.activeMode === 'file') return; mergeState.activeMode = 'file'; filePanel.classList.remove('hidden'); pagePanel.classList.add('hidden'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.remove('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); }); newPageModeBtn.addEventListener('click', async () => { if (mergeState.activeMode === 'page') return; mergeState.activeMode = 'page'; filePanel.classList.add('hidden'); pagePanel.classList.remove('hidden'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); await renderPageMergeThumbnails(); }); if (wasInPageMode) { mergeState.activeMode = 'page'; filePanel.classList.add('hidden'); pagePanel.classList.remove('hidden'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); await renderPageMergeThumbnails(); } else { // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); // @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); } }