2026-01-27 15:26:11 +05:30
|
|
|
import { downloadFile, formatBytes } from '../utils/helpers';
|
|
|
|
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
|
|
|
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
|
|
|
|
import {
|
|
|
|
|
showWasmRequiredDialog,
|
|
|
|
|
WasmProvider,
|
|
|
|
|
} from '../utils/wasm-provider.js';
|
2026-03-26 12:11:12 +05:30
|
|
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
2026-01-27 15:26:11 +05:30
|
|
|
|
|
|
|
|
const worker = new Worker(
|
|
|
|
|
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
|
|
|
|
|
);
|
2025-11-08 12:37:10 +05:30
|
|
|
|
|
|
|
|
let pdfFile: File | null = null;
|
|
|
|
|
|
|
|
|
|
const dropZone = document.getElementById('drop-zone') as HTMLElement;
|
|
|
|
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
2025-11-08 13:17:29 +05:30
|
|
|
const generateBtn = document.getElementById(
|
|
|
|
|
'generate-btn'
|
|
|
|
|
) as HTMLButtonElement;
|
2025-11-08 12:37:10 +05:30
|
|
|
const tocTitleInput = document.getElementById('toc-title') as HTMLInputElement;
|
2025-11-08 13:17:29 +05:30
|
|
|
const fontSizeSelect = document.getElementById(
|
|
|
|
|
'font-size'
|
|
|
|
|
) as HTMLSelectElement;
|
|
|
|
|
const fontFamilySelect = document.getElementById(
|
|
|
|
|
'font-family'
|
|
|
|
|
) as HTMLSelectElement;
|
|
|
|
|
const addBookmarkCheckbox = document.getElementById(
|
|
|
|
|
'add-bookmark'
|
|
|
|
|
) as HTMLInputElement;
|
2025-11-08 12:37:10 +05:30
|
|
|
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
2025-11-08 13:17:29 +05:30
|
|
|
const fileDisplayArea = document.getElementById(
|
|
|
|
|
'file-display-area'
|
|
|
|
|
) as HTMLElement;
|
|
|
|
|
const backToToolsBtn = document.getElementById(
|
|
|
|
|
'back-to-tools'
|
|
|
|
|
) as HTMLButtonElement;
|
2025-11-08 12:37:10 +05:30
|
|
|
|
|
|
|
|
interface TOCSuccessResponse {
|
|
|
|
|
status: 'success';
|
|
|
|
|
pdfBytes: ArrayBuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TOCErrorResponse {
|
|
|
|
|
status: 'error';
|
|
|
|
|
message: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TOCWorkerResponse = TOCSuccessResponse | TOCErrorResponse;
|
|
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
function showStatus(
|
|
|
|
|
message: string,
|
|
|
|
|
type: 'success' | 'error' | 'info' = 'info'
|
|
|
|
|
) {
|
2025-11-08 12:37:10 +05:30
|
|
|
statusMessage.textContent = message;
|
2026-01-27 15:26:11 +05:30
|
|
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
|
|
|
|
type === 'success'
|
|
|
|
|
? 'bg-green-900 text-green-200'
|
|
|
|
|
: type === 'error'
|
|
|
|
|
? 'bg-red-900 text-red-200'
|
|
|
|
|
: 'bg-blue-900 text-blue-200'
|
|
|
|
|
}`;
|
2025-11-08 12:37:10 +05:30
|
|
|
statusMessage.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideStatus() {
|
|
|
|
|
statusMessage.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
function renderFileDisplay(file: File) {
|
|
|
|
|
fileDisplayArea.innerHTML = '';
|
|
|
|
|
fileDisplayArea.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
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 sizeSpan = document.createElement('span');
|
|
|
|
|
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
|
|
|
|
|
sizeSpan.textContent = formatBytes(file.size);
|
|
|
|
|
|
|
|
|
|
fileDiv.append(nameSpan, sizeSpan);
|
|
|
|
|
fileDisplayArea.appendChild(fileDiv);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 12:11:12 +05:30
|
|
|
async function handleFileSelect(file: File) {
|
2025-11-08 12:37:10 +05:30
|
|
|
if (file.type !== 'application/pdf') {
|
|
|
|
|
showStatus('Please select a PDF file.', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 12:11:12 +05:30
|
|
|
const result = await loadPdfWithPasswordPrompt(file);
|
|
|
|
|
if (!result) return;
|
|
|
|
|
result.pdf.destroy();
|
|
|
|
|
pdfFile = result.file;
|
2025-11-08 12:37:10 +05:30
|
|
|
generateBtn.disabled = false;
|
2026-03-26 12:11:12 +05:30
|
|
|
renderFileDisplay(pdfFile);
|
2025-11-08 12:37:10 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.add('border-blue-500');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
|
|
|
dropZone.classList.remove('border-blue-500');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.remove('border-blue-500');
|
|
|
|
|
const file = e.dataTransfer?.files[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
handleFileSelect(file);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
|
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
handleFileSelect(file);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function generateTableOfContents() {
|
|
|
|
|
if (!pdfFile) {
|
|
|
|
|
showStatus('Please select a PDF file first.', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 15:26:11 +05:30
|
|
|
// Check if CPDF is configured
|
|
|
|
|
if (!isCpdfAvailable()) {
|
|
|
|
|
showWasmRequiredDialog('cpdf');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-08 12:37:10 +05:30
|
|
|
try {
|
|
|
|
|
generateBtn.disabled = true;
|
|
|
|
|
showStatus('Reading file (Main Thread)...', 'info');
|
|
|
|
|
|
|
|
|
|
const arrayBuffer = await pdfFile.arrayBuffer();
|
|
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
showStatus('Generating table of contents...', 'info');
|
2025-11-08 12:37:10 +05:30
|
|
|
|
|
|
|
|
const title = tocTitleInput.value || 'Table of Contents';
|
|
|
|
|
const fontSize = parseInt(fontSizeSelect.value, 10);
|
|
|
|
|
const fontFamily = parseInt(fontFamilySelect.value, 10);
|
|
|
|
|
const addBookmark = addBookmarkCheckbox.checked;
|
|
|
|
|
|
2026-01-27 15:26:11 +05:30
|
|
|
const message = {
|
2025-11-08 12:37:10 +05:30
|
|
|
command: 'generate-toc',
|
|
|
|
|
pdfData: arrayBuffer,
|
|
|
|
|
title,
|
|
|
|
|
fontSize,
|
|
|
|
|
fontFamily,
|
|
|
|
|
addBookmark,
|
2026-01-27 15:26:11 +05:30
|
|
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
2025-11-08 12:37:10 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
worker.postMessage(message, [arrayBuffer]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error reading file:', error);
|
|
|
|
|
showStatus(
|
|
|
|
|
`Error reading file: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
|
|
|
|
'error'
|
|
|
|
|
);
|
|
|
|
|
generateBtn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
worker.onmessage = (e: MessageEvent<TOCWorkerResponse>) => {
|
|
|
|
|
generateBtn.disabled = false;
|
|
|
|
|
|
|
|
|
|
if (e.data.status === 'success') {
|
|
|
|
|
const pdfBytesBuffer = e.data.pdfBytes;
|
|
|
|
|
const pdfBytes = new Uint8Array(pdfBytesBuffer);
|
|
|
|
|
|
|
|
|
|
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
2026-01-27 15:26:11 +05:30
|
|
|
downloadFile(
|
|
|
|
|
blob,
|
|
|
|
|
pdfFile?.name.replace('.pdf', '_with_toc.pdf') || 'output_with_toc.pdf'
|
|
|
|
|
);
|
2025-11-08 12:37:10 +05:30
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
showStatus(
|
|
|
|
|
'Table of contents generated successfully! Download started.',
|
|
|
|
|
'success'
|
|
|
|
|
);
|
2025-11-08 12:37:10 +05:30
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
hideStatus();
|
|
|
|
|
pdfFile = null;
|
|
|
|
|
fileInput.value = '';
|
|
|
|
|
fileDisplayArea.innerHTML = '';
|
|
|
|
|
fileDisplayArea.classList.add('hidden');
|
|
|
|
|
generateBtn.disabled = true;
|
2025-11-08 12:37:10 +05:30
|
|
|
} else if (e.data.status === 'error') {
|
|
|
|
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
|
|
|
|
console.error('Worker Error:', errorMessage);
|
|
|
|
|
showStatus(`Error: ${errorMessage}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
worker.onerror = (error) => {
|
|
|
|
|
console.error('Worker error:', error);
|
|
|
|
|
showStatus('Worker error occurred. Check console for details.', 'error');
|
|
|
|
|
generateBtn.disabled = false;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-08 13:17:29 +05:30
|
|
|
if (backToToolsBtn) {
|
|
|
|
|
backToToolsBtn.addEventListener('click', () => {
|
2025-12-04 23:53:00 +05:30
|
|
|
window.location.href = import.meta.env.BASE_URL;
|
2025-11-08 13:17:29 +05:30
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
generateBtn.addEventListener('click', generateTableOfContents);
|
2025-11-21 17:10:56 +05:30
|
|
|
|
|
|
|
|
initializeGlobalShortcuts();
|