/** * Markdown Editor Module * UPDATED: Data-driven section markers (stored in editorBlocks array) * REMOVED: duplicate renderDocumentBlocks (now only in pdf-handler.js) * FIXED: repairAllNewBlockLines no longer removes lines after section-dividers * FIXED: removeSection is data-driven, no orphan dividers * ADDED: addSectionAtLine function for Custom Section Marker button */ // ============================================ // Editor State // ============================================ let editorBlocks = []; let activeBlockId = null; let isToolbarClick = false; // Panel state let panelState = { startingBlockId: null, blockCount: 10, voice: 'af_heart', pickMode: false }; // ============================================ // Initialization // ============================================ function initMarkdownEditor() { const editor = document.getElementById('markdownEditor'); editor.addEventListener('click', function(e) { // Pick mode: clicking a block sets it as starting block if (panelState.pickMode) { const blockEl = e.target.closest('.md-block'); if (blockEl && !blockEl.classList.contains('editing')) { setStartingBlock(blockEl.id); exitPickMode(); e.preventDefault(); e.stopPropagation(); return; } } if (e.target === editor || e.target.id === 'emptyEditorMessage') { if (editorBlocks.length === 0) { addBlock('paragraph', ''); repairAllNewBlockLines(); updatePanelUI(); } } }); document.addEventListener('keydown', handleGlobalKeydown); document.addEventListener('mousedown', function(e) { if (e.target.closest('.md-block-toolbar') || e.target.closest('.dropdown-menu') || e.target.closest('.toolbar-btn') || e.target.closest('.btn-merge-section')) { isToolbarClick = true; } else { isToolbarClick = false; } }); restorePanelSettings(); console.log('đ Markdown editor initialized (data-driven sections)'); } // ============================================ // Audiobook Maker Panel // ============================================ function initAudiobookMakerPanel() { updatePanelUI(); } function updatePanelUI() { const textBlocks = getTextBlocks(); const totalBlocks = textBlocks.length; // Update total blocks stat with NULL checks const totalEl = document.getElementById('ampTotalBlocks'); if (totalEl) totalEl.textContent = totalBlocks; const genCount = textBlocks.filter(b => { const data = editorBlocks.find(eb => eb.id === b.id); return data && data.audio_data; }).length; const genEl = document.getElementById('ampGeneratedBlocks'); if (genEl) genEl.textContent = genCount; const remainEl = document.getElementById('ampRemainingBlocks'); if (remainEl) remainEl.textContent = (totalBlocks - genCount); // Validate starting block if (!panelState.startingBlockId || !document.getElementById(panelState.startingBlockId)) { if (textBlocks.length > 0) { panelState.startingBlockId = textBlocks[0].id; } else { panelState.startingBlockId = null; } } updateStartingBlockDisplay(); updateBlockCountLimits(); updateBlockHighlights(); } function getTextBlocks() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; const allBlocks = editor.querySelectorAll('.md-block'); const textBlocks = []; allBlocks.forEach(block => { const blockType = block.dataset.blockType || 'paragraph'; if (blockType !== 'image') { const textarea = block.querySelector('.md-block-textarea'); const content = textarea ? textarea.value.trim() : ''; const isImageContent = content.startsWith('; if (!isImageContent) { textBlocks.push(block); } } }); return textBlocks; } function getAllContentBlocks() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; return Array.from(editor.querySelectorAll('.md-block')); } function getTextBlockIndex(blockId) { const textBlocks = getTextBlocks(); return textBlocks.findIndex(b => b.id === blockId); } function updateStartingBlockDisplay() { const numEl = document.getElementById('ampStartBlockNum'); if (!numEl) return; if (panelState.startingBlockId) { const idx = getTextBlockIndex(panelState.startingBlockId); numEl.textContent = idx >= 0 ? (idx + 1) : '-'; } else { numEl.textContent = '-'; } } function updateBlockCountLimits() { const input = document.getElementById('ampBlockCount'); if (!input) return; const textBlocks = getTextBlocks(); const startIdx = panelState.startingBlockId ? getTextBlockIndex(panelState.startingBlockId) : 0; let remaining = 0; if (startIdx >= 0) { remaining = textBlocks.length - startIdx; } const maxVal = Math.max(1, remaining); input.max = maxVal; if (panelState.blockCount > maxVal) panelState.blockCount = maxVal; if (panelState.blockCount < 1 && maxVal >= 1) panelState.blockCount = Math.min(10, maxVal); input.value = panelState.blockCount; } function updateBlockHighlights() { document.querySelectorAll('.md-block.starting-block').forEach(b => b.classList.remove('starting-block')); document.querySelectorAll('.md-block.in-gen-range').forEach(b => b.classList.remove('in-gen-range')); if (!panelState.startingBlockId) return; const startEl = document.getElementById(panelState.startingBlockId); if (!startEl) return; startEl.classList.add('starting-block'); const textBlocks = getTextBlocks(); const startIdx = getTextBlockIndex(panelState.startingBlockId); if (startIdx >= 0) { const endIdx = Math.min(startIdx + panelState.blockCount, textBlocks.length); for (let i = startIdx; i < endIdx; i++) { if (i !== startIdx) { textBlocks[i].classList.add('in-gen-range'); } } } } function setStartingBlock(blockId) { panelState.startingBlockId = blockId; updatePanelUI(); savePanelSettings(); } function enterPickMode() { panelState.pickMode = true; const editor = document.getElementById('markdownEditor'); if (editor) editor.classList.add('editor-pick-mode'); const indicator = document.getElementById('ampStartIndicator'); if (indicator) indicator.classList.add('pick-mode'); } function exitPickMode() { panelState.pickMode = false; const editor = document.getElementById('markdownEditor'); if (editor) editor.classList.remove('editor-pick-mode'); const indicator = document.getElementById('ampStartIndicator'); if (indicator) indicator.classList.remove('pick-mode'); } function togglePickMode() { if (panelState.pickMode) { exitPickMode(); } else { enterPickMode(); } } function handleBlockCountChange(value) { const num = parseInt(value); if (isNaN(num) || num < 1) return; const input = document.getElementById('ampBlockCount'); const maxVal = parseInt(input.max) || 999; panelState.blockCount = Math.min(num, maxVal); if (input) input.value = panelState.blockCount; updateBlockHighlights(); savePanelSettings(); } function adjustBlockCount(delta) { const input = document.getElementById('ampBlockCount'); const current = input ? (parseInt(input.value) || 1) : 1; const maxVal = input ? (parseInt(input.max) || 999) : 999; const newVal = Math.max(1, Math.min(current + delta, maxVal)); panelState.blockCount = newVal; if (input) input.value = newVal; updateBlockHighlights(); savePanelSettings(); } function handleVoiceChange(value) { panelState.voice = value; savePanelSettings(); } function savePanelSettings() { sessionStorage.setItem('ampSettings', JSON.stringify({ blockCount: panelState.blockCount, voice: panelState.voice })); } function restorePanelSettings() { const saved = sessionStorage.getItem('ampSettings'); if (saved) { try { const settings = JSON.parse(saved); if (settings.blockCount) panelState.blockCount = settings.blockCount; if (settings.voice) panelState.voice = settings.voice; } catch(e) { /* ignore */ } } } function advanceStartingBlockAfterGeneration(generatedCount) { const textBlocks = getTextBlocks(); const startIdx = getTextBlockIndex(panelState.startingBlockId); if (startIdx >= 0) { const nextIdx = startIdx + generatedCount; if (nextIdx < textBlocks.length) { panelState.startingBlockId = textBlocks[nextIdx].id; } else { if (textBlocks.length > 0) { panelState.startingBlockId = textBlocks[textBlocks.length - 1].id; } } } updatePanelUI(); savePanelSettings(); } // ============================================ // DATA-DRIVEN Section Marker System // ============================================ function makeSectionStart(blockId, title = null) { const blockData = editorBlocks.find(b => b.id === blockId); if (!blockData) return; if (blockData.sectionStart) return; const sectionNum = editorBlocks.filter(b => b.sectionStart).length + 1; blockData.sectionStart = true; blockData.sectionName = title || `Section ${sectionNum}`; renderAllSectionDividers(); renderDocumentOutline(); } function removeSection(blockId) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.sectionStart = false; blockData.sectionName = ''; } renderAllSectionDividers(); renderDocumentOutline(); } function renderAllSectionDividers() { const editor = document.getElementById('markdownEditor'); if (!editor) return; editor.querySelectorAll('.section-divider').forEach(d => d.remove()); for (const blockData of editorBlocks) { if (!blockData.sectionStart) continue; const blockEl = document.getElementById(blockData.id); if (!blockEl) continue; const divider = document.createElement('div'); divider.className = 'section-divider'; divider.dataset.targetBlock = blockData.id; divider.innerHTML = `
Click to upload an image
${escapeHtml(content)}
`; } } // ============================================ // New Block Line & DOM Cleanup // ============================================ function createNewBlockLine() { const line = document.createElement('div'); line.className = 'new-block-line'; const lineId = 'line_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); line.innerHTML = ` `; return line; } function addBlockAtLine(button) { const line = button.closest('.new-block-line'); addBlock('paragraph', '', line); repairAllNewBlockLines(); } function handleImageAtLine(event, inputEl) { const file = event.target.files[0]; if (!file || !file.type.startsWith('image/')) return; const line = inputEl.closest('.new-block-line'); if (!line) return; const reader = new FileReader(); reader.onload = function(e) { const base64Data = e.target.result.split(',')[1]; const format = file.type.split('/')[1] === 'jpeg' ? 'jpeg' : file.type.split('/')[1]; const images = [{ data: base64Data, format: format, alt_text: file.name, position: 'before' }]; const content = ``; addBlock('image', content, line, images); repairAllNewBlockLines(); updatePanelUI(); }; reader.readAsDataURL(file); event.target.value = ''; } function addSectionAtLine(button) { const line = button.closest('.new-block-line'); const blockId = addBlock('heading2', 'New Section Heading', line); makeSectionStart(blockId, 'New Section Heading'); repairAllNewBlockLines(); enterEditMode(blockId); } function ensureNewBlockLineAfter(element) { if (!element) return; const next = element.nextElementSibling; if (next && next.classList.contains('new-block-line')) { return; } element.after(createNewBlockLine()); } function repairAllNewBlockLines() { const editor = document.getElementById('markdownEditor'); if (!editor) return; const children = Array.from(editor.children); for (let i = 0; i < children.length; i++) { const el = children[i]; if (el.classList.contains('new-block-line')) { const prev = el.previousElementSibling; if (!prev || prev.classList.contains('new-block-line')) { el.remove(); } } } const updatedChildren = Array.from(editor.children); for (let i = 0; i < updatedChildren.length; i++) { const el = updatedChildren[i]; if (el.classList.contains('md-block')) { ensureNewBlockLineAfter(el); } } } function checkEmptyEditor() { const editor = document.getElementById('markdownEditor'); if(!editor) return; const blocks = editor.querySelectorAll('.md-block'); const emptyMessage = document.getElementById('emptyEditorMessage'); const panel = document.getElementById('audiobookMakerPanel'); const sidebar = document.getElementById('documentOutlineSidebar'); if (blocks.length === 0) { editor.querySelectorAll('.new-block-line').forEach(line => line.remove()); editor.querySelectorAll('.section-divider').forEach(div => div.remove()); if (emptyMessage) emptyMessage.style.display = 'block'; if (panel) panel.style.display = 'none'; if (sidebar) sidebar.style.display = 'none'; } else { if (emptyMessage) emptyMessage.style.display = 'none'; if (panel) panel.style.display = 'flex'; if (sidebar) sidebar.style.display = 'block'; } } // ============================================ // Content Collection // ============================================ function collectEditorContent() { const editor = document.getElementById('markdownEditor'); if (!editor) return []; const chapters = []; let currentChapter = null; let blockOrder = 0; let sectionCounter = 0; const allBlockEls = editor.querySelectorAll('.md-block'); for (const el of allBlockEls) { const blockData = editorBlocks.find(b => b.id === el.id); if (!blockData) continue; if (blockData.sectionStart) { if (currentChapter && currentChapter.blocks.length > 0) { chapters.push(currentChapter); } sectionCounter++; currentChapter = { chapter_number: sectionCounter, title: blockData.sectionName || `Section ${sectionCounter}`, voice: panelState.voice || 'af_heart', blocks: [] }; blockOrder = 0; } if (!currentChapter) { sectionCounter++; const firstContent = el.querySelector('.md-block-textarea') ? el.querySelector('.md-block-textarea').value.trim() : ''; const plainText = stripMarkdown(firstContent); const snippet = plainText ? (plainText.substring(0, 40) + (plainText.length > 40 ? '...' : '')) : 'Section 1'; currentChapter = { chapter_number: sectionCounter, title: blockData.sectionName || snippet, voice: panelState.voice || 'af_heart', blocks: [] }; } blockOrder++; const textarea = el.querySelector('.md-block-textarea'); const content = textarea ? textarea.value : ''; const blockType = el.dataset.blockType || 'paragraph'; if (content.trim() || blockType === 'image') { currentChapter.blocks.push({ block_order: blockOrder, block_type: blockType, content: content, tts_text: el.dataset.ttsText || '', audio_data: blockData.audio_data || '', audio_format: blockData.audio_format || 'mp3', transcription: blockData.transcription || [], images: blockData.images || [] }); } } if (currentChapter && currentChapter.blocks.length > 0) { chapters.push(currentChapter); } return chapters; } function renderProjectInEditor(projectData) { const editor = document.getElementById('markdownEditor'); if(!editor) return; editor.innerHTML = ''; editorBlocks = []; const emptyMessage = document.getElementById('emptyEditorMessage'); if (emptyMessage) emptyMessage.style.display = 'none'; if (projectData.chapters && projectData.chapters.length > 0 && projectData.chapters[0].voice) { panelState.voice = projectData.chapters[0].voice; const voiceSelect = document.getElementById('ampVoiceSelect'); if (voiceSelect) voiceSelect.value = panelState.voice; } for (const chapter of projectData.chapters) { let isFirstInChapter = true; for (const block of chapter.blocks) { const lastChild = editor.lastElementChild; const blockId = addBlock( block.block_type, block.content, lastChild, block.images || [] ); if (isFirstInChapter) { const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.sectionStart = true; blockData.sectionName = chapter.title || 'Section'; } isFirstInChapter = false; } const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.audio_data = block.audio_data; blockData.audio_format = block.audio_format; blockData.transcription = block.transcription; blockData.tts_text = block.tts_text; } const blockEl = document.getElementById(blockId); if (blockEl && block.tts_text) { blockEl.dataset.ttsText = block.tts_text; } if (block.audio_data && block.block_type !== 'image') { if (blockEl) { const indicator = blockEl.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio generated'; } } } if (blockEl) { ensureNewBlockLineAfter(blockEl); } } } renderAllSectionDividers(); repairAllNewBlockLines(); const textBlocks = getTextBlocks(); let foundStart = false; for (const tb of textBlocks) { const data = editorBlocks.find(b => b.id === tb.id); if (!data || !data.audio_data) { panelState.startingBlockId = tb.id; foundStart = true; break; } } if (!foundStart && textBlocks.length > 0) { panelState.startingBlockId = textBlocks[textBlocks.length - 1].id; } // āϝā§āĻā§āϤ āĻāϰāĻž āĻšāϞā§: āĻĒā§āϰāĻā§āĻā§āĻ āϞā§āĻĄ āĻšāϞā§āĻ āĻĒā§āϝāĻžāύā§āϞā§āϰ āĻŦā§āϞāĻ āĻāĻžāĻāύā§āĻ āϝā§āύ āĻŽā§āĻ āĻŦā§āϞāĻā§āϰ āϏāĻŽāĻžāύ āĻĨāĻžāĻā§ if (textBlocks.length > 0) { panelState.blockCount = textBlocks.length; } updatePanelUI(); renderDocumentOutline(); checkEmptyEditor(); }