Audiobook Maker Pro v4.2 — production ready
This commit is contained in:
270
static/js/pdf-handler.js
Normal file
270
static/js/pdf-handler.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Document Handler Module (PDF, DOCX, DOC)
|
||||
* AUTHORITATIVE renderDocumentBlocks() — single source of truth
|
||||
* Section markers are data-driven via editorBlocks[].sectionStart
|
||||
*/
|
||||
|
||||
function initPdfHandler() {
|
||||
const uploadZone = document.getElementById('uploadZone');
|
||||
const docInput = document.getElementById('docInput');
|
||||
|
||||
if (!uploadZone || !docInput) return;
|
||||
|
||||
uploadZone.addEventListener('click', (e) => {
|
||||
if (e.target.closest('button')) return;
|
||||
docInput.click();
|
||||
});
|
||||
|
||||
docInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
handleDocumentFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('drag-over');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', () => {
|
||||
uploadZone.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('drag-over');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
const name = file.name.toLowerCase();
|
||||
|
||||
if (name.endsWith('.pdf') || name.endsWith('.docx') || name.endsWith('.doc')) {
|
||||
handleDocumentFile(file);
|
||||
} else {
|
||||
alert('Please drop a valid PDF, DOCX, or DOC file.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📄 Document handler initialized (PDF, DOCX, DOC)');
|
||||
}
|
||||
|
||||
function handleDocumentFile(file) {
|
||||
const name = file.name.toLowerCase();
|
||||
|
||||
if (name.endsWith('.pdf')) {
|
||||
handlePdfFile(file);
|
||||
} else if (name.endsWith('.docx') || name.endsWith('.doc')) {
|
||||
handleWordFile(file);
|
||||
} else {
|
||||
alert('Unsupported file type. Please upload a PDF, DOCX, or DOC file.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePdfFile(file) {
|
||||
if (!file.name.toLowerCase().endsWith('.pdf')) {
|
||||
alert('Please select a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Processing PDF...', `Extracting content from ${file.name}`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload-pdf', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
console.log(`✅ PDF processed: ${data.page_count} pages, ${data.blocks.length} blocks`);
|
||||
|
||||
const projectName = file.name.replace('.pdf', '');
|
||||
document.getElementById('projectName').value = projectName;
|
||||
currentProject.name = projectName;
|
||||
|
||||
renderDocumentBlocks(data.blocks);
|
||||
|
||||
document.getElementById('uploadSection').style.display = 'none';
|
||||
document.getElementById('editorSection').style.display = 'block';
|
||||
|
||||
const panel = document.getElementById('audiobookMakerPanel');
|
||||
if (panel) panel.style.display = 'flex';
|
||||
|
||||
const sidebar = document.getElementById('documentOutlineSidebar');
|
||||
if (sidebar) sidebar.style.display = 'block';
|
||||
|
||||
hideLoader();
|
||||
showNotification(`PDF processed: ${data.blocks.length} blocks extracted`, 'success');
|
||||
updateWorkflowProgress('edit');
|
||||
|
||||
} catch (error) {
|
||||
hideLoader();
|
||||
console.error('PDF processing error:', error);
|
||||
alert('Failed to process PDF: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWordFile(file) {
|
||||
const name = file.name.toLowerCase();
|
||||
if (!name.endsWith('.docx') && !name.endsWith('.doc')) {
|
||||
alert('Please select a valid DOCX or DOC file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileType = name.endsWith('.docx') ? 'DOCX' : 'DOC';
|
||||
showLoader(`Processing ${fileType}...`, `Extracting content from ${file.name}`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload-docx', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
console.log(`✅ ${fileType} processed: ${data.blocks.length} blocks`);
|
||||
|
||||
const projectName = file.name.replace(/\.(docx|doc)$/i, '');
|
||||
document.getElementById('projectName').value = projectName;
|
||||
currentProject.name = projectName;
|
||||
|
||||
renderDocumentBlocks(data.blocks);
|
||||
|
||||
document.getElementById('uploadSection').style.display = 'none';
|
||||
document.getElementById('editorSection').style.display = 'block';
|
||||
|
||||
const panel = document.getElementById('audiobookMakerPanel');
|
||||
if (panel) panel.style.display = 'flex';
|
||||
|
||||
const sidebar = document.getElementById('documentOutlineSidebar');
|
||||
if (sidebar) sidebar.style.display = 'block';
|
||||
|
||||
hideLoader();
|
||||
showNotification(`${fileType} processed: ${data.blocks.length} blocks extracted`, 'success');
|
||||
updateWorkflowProgress('edit');
|
||||
|
||||
} catch (error) {
|
||||
hideLoader();
|
||||
console.error(`${fileType} processing error:`, error);
|
||||
alert(`Failed to process ${fileType}: ` + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AUTHORITATIVE renderDocumentBlocks()
|
||||
* This is the ONLY version of this function in the entire app.
|
||||
* Section dividers are rendered from editorBlocks[].sectionStart data.
|
||||
*/
|
||||
function renderDocumentBlocks(blocks) {
|
||||
const editor = document.getElementById('markdownEditor');
|
||||
if (!editor) return;
|
||||
|
||||
editor.innerHTML = '';
|
||||
editorBlocks = [];
|
||||
|
||||
const emptyMessage = document.getElementById('emptyEditorMessage');
|
||||
if (emptyMessage) {
|
||||
emptyMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
let type = 'paragraph';
|
||||
let content = block.content || '';
|
||||
|
||||
if (block.type === 'image') {
|
||||
type = 'image';
|
||||
} else if (block.type === 'heading1' || content.startsWith('# ')) {
|
||||
type = 'heading1';
|
||||
} else if (block.type === 'heading2' || content.startsWith('## ')) {
|
||||
type = 'heading2';
|
||||
} else if (block.type === 'heading3' || content.startsWith('### ')) {
|
||||
type = 'heading3';
|
||||
} else if (block.type === 'list_item' || content.startsWith('- ')) {
|
||||
type = 'bulletList';
|
||||
} else if (block.type === 'quote' || content.startsWith('> ')) {
|
||||
type = 'quote';
|
||||
} else if (block.type === 'table') {
|
||||
type = 'table';
|
||||
}
|
||||
|
||||
let images = [];
|
||||
if (block.type === 'image' && block.data) {
|
||||
images = [{
|
||||
data: block.data,
|
||||
format: block.format || 'png',
|
||||
alt_text: 'Document Image',
|
||||
position: 'before'
|
||||
}];
|
||||
content = ``;
|
||||
}
|
||||
|
||||
const lastChild = editor.lastElementChild;
|
||||
const blockId = addBlock(type, content, lastChild, images);
|
||||
|
||||
// Store section info in editorBlocks data (data-driven approach)
|
||||
if (block.is_section_start) {
|
||||
const blockData = editorBlocks.find(b => b.id === blockId);
|
||||
if (blockData) {
|
||||
blockData.sectionStart = true;
|
||||
blockData.sectionName = block.section_name || 'Section';
|
||||
}
|
||||
}
|
||||
|
||||
if (block.type === 'image' && block.data) {
|
||||
const blockEl = document.getElementById(blockId);
|
||||
if (blockEl) {
|
||||
const contentDiv = blockEl.querySelector('.md-block-content');
|
||||
if (contentDiv) {
|
||||
contentDiv.innerHTML = `
|
||||
<div class="image-block">
|
||||
<img src="data:image/${block.format || 'png'};base64,${block.data}" alt="Document Image">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blockEl = document.getElementById(blockId);
|
||||
if (blockEl) {
|
||||
ensureNewBlockLineAfter(blockEl);
|
||||
}
|
||||
}
|
||||
|
||||
// Now render all section dividers from data
|
||||
renderAllSectionDividers();
|
||||
|
||||
repairAllNewBlockLines();
|
||||
|
||||
// Initialize panel state for new document
|
||||
const textBlocks = getTextBlocks();
|
||||
if (textBlocks.length > 0) {
|
||||
panelState.startingBlockId = textBlocks[0].id;
|
||||
panelState.blockCount = textBlocks.length; // Modified: set to total blocks
|
||||
}
|
||||
|
||||
updatePanelUI();
|
||||
renderDocumentOutline();
|
||||
checkEmptyEditor();
|
||||
}
|
||||
|
||||
// Keep backward compatibility alias
|
||||
function renderPdfBlocks(blocks) {
|
||||
renderDocumentBlocks(blocks);
|
||||
}
|
||||
Reference in New Issue
Block a user