/** * Audiobook Maker Pro v4.2 - Main Application * UPDATED: Lazy audio loading to avoid large response truncation */ // ============================================ // Global State // ============================================ let currentProject = { id: null, name: 'My Audiobook', chapters: [] }; let voices = []; let archiveModal = null; let ttsEditModal = null; let publishModal = null; let publishingProjectId = null; let currentWorkflowStage = 'upload'; let allArchiveProjects = []; // ============================================ // Initialization // ============================================ document.addEventListener('DOMContentLoaded', function() { console.log('🎧 Audiobook Maker Pro v4.2 initializing...'); archiveModal = new bootstrap.Modal(document.getElementById('archiveModal')); ttsEditModal = new bootstrap.Modal(document.getElementById('ttsEditModal')); loadVoices(); initPdfHandler(); initMarkdownEditor(); setupEventListeners(); initWelcomeOverlay(); updateWorkflowProgress('upload'); initFloatingGuidePanel(); loadCurrentUser(); if (typeof hideReaderUI === 'function') { hideReaderUI(); } console.log('✅ Application initialized'); }); function setupEventListeners() { document.getElementById('projectName').addEventListener('change', function() { currentProject.name = this.value; }); document.getElementById('reader-tab').addEventListener('shown.bs.tab', function() { renderInteractiveReader(); if (typeof showReaderUI === 'function') { showReaderUI(); } hideGuidePanelForReader(); }); document.getElementById('editor-tab').addEventListener('shown.bs.tab', function() { if (typeof hideReaderUI === 'function') { hideReaderUI(); } restoreGuidePanelForEditor(); }); } // ============================================ // Welcome Overlay // ============================================ function initWelcomeOverlay() { const dontShow = localStorage.getItem('audiobookMakerHideWelcome'); if (dontShow === 'true') { document.getElementById('welcomeOverlay').style.display = 'none'; } else { document.getElementById('welcomeOverlay').style.display = 'flex'; } } function dismissWelcome() { const dontShowCheckbox = document.getElementById('welcomeDontShow'); if (dontShowCheckbox && dontShowCheckbox.checked) { localStorage.setItem('audiobookMakerHideWelcome', 'true'); } document.getElementById('welcomeOverlay').style.display = 'none'; } function showWelcome() { document.getElementById('welcomeOverlay').style.display = 'flex'; } // ============================================ // Dynamic Header Help Button // ============================================ function handleHeaderHelp() { if (currentWorkflowStage === 'upload') { showWelcome(); } else { showGuidePanel(); } } function updateHeaderHelpButton(stage) { const label = document.getElementById('headerHelpLabel'); const btn = document.getElementById('headerHelpBtn'); if (!label || !btn) return; if (stage === 'upload') { label.textContent = 'Quick Start'; btn.title = 'Show quick start guide'; } else { label.textContent = 'Quick Guide'; btn.title = 'Show editor quick guide'; } } // ============================================ // Floating Guide Panel // ============================================ let guidePanelDragState = { isDragging: false, startX: 0, startY: 0, offsetX: 0, offsetY: 0 }; function initFloatingGuidePanel() { const panel = document.getElementById('floatingGuidePanel'); const header = document.getElementById('guidePanelHeader'); const toggle = document.getElementById('floatingGuideToggle'); if (!panel || !header) return; const hideGuide = localStorage.getItem('audiobookMakerHideGuide'); if (hideGuide === 'true') { panel.classList.remove('visible'); if (toggle) toggle.classList.add('visible'); return; } const collapsed = localStorage.getItem('audiobookMakerGuideCollapsed'); if (collapsed === 'true') { panel.classList.add('collapsed'); const icon = document.getElementById('guideCollapseIcon'); if (icon) { icon.classList.remove('bi-chevron-up'); icon.classList.add('bi-chevron-down'); } } const savedPos = localStorage.getItem('audiobookMakerGuidePos'); if (savedPos) { try { const pos = JSON.parse(savedPos); const maxX = window.innerWidth - 100; const maxY = window.innerHeight - 50; if (pos.x >= 0 && pos.x <= maxX && pos.y >= 0 && pos.y <= maxY) { panel.style.right = 'auto'; panel.style.left = pos.x + 'px'; panel.style.top = pos.y + 'px'; } } catch(e) { /* ignore */ } } header.addEventListener('mousedown', onGuideDragStart); document.addEventListener('mousemove', onGuideDragMove); document.addEventListener('mouseup', onGuideDragEnd); header.addEventListener('touchstart', onGuideTouchStart, { passive: false }); document.addEventListener('touchmove', onGuideTouchMove, { passive: false }); document.addEventListener('touchend', onGuideTouchEnd); } function showGuidePanelOnEditor() { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); const hideGuide = localStorage.getItem('audiobookMakerHideGuide'); if (hideGuide === 'true') { if (toggle) toggle.classList.add('visible'); return; } if (panel) panel.classList.add('visible'); if (toggle) toggle.classList.remove('visible'); } function showGuidePanel() { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); if (panel) panel.classList.add('visible'); if (toggle) toggle.classList.remove('visible'); localStorage.removeItem('audiobookMakerHideGuide'); } function hideGuidePanel() { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); if (panel) panel.classList.remove('visible'); if (toggle) toggle.classList.add('visible'); } function toggleGuideCollapse() { const panel = document.getElementById('floatingGuidePanel'); const icon = document.getElementById('guideCollapseIcon'); if (!panel) return; const isCollapsed = panel.classList.toggle('collapsed'); if (icon) { if (isCollapsed) { icon.classList.remove('bi-chevron-up'); icon.classList.add('bi-chevron-down'); } else { icon.classList.remove('bi-chevron-down'); icon.classList.add('bi-chevron-up'); } } localStorage.setItem('audiobookMakerGuideCollapsed', isCollapsed ? 'true' : 'false'); } function handleGuideDontShow() { const checkbox = document.getElementById('guidePanelDontShow'); if (checkbox && checkbox.checked) { localStorage.setItem('audiobookMakerHideGuide', 'true'); hideGuidePanel(); } else { localStorage.removeItem('audiobookMakerHideGuide'); } } let guidePanelHiddenByReader = false; function hideGuidePanelForReader() { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); if (panel && panel.classList.contains('visible')) { panel.classList.remove('visible'); guidePanelHiddenByReader = true; } else { guidePanelHiddenByReader = false; } if (toggle) toggle.classList.add('visible'); } function restoreGuidePanelForEditor() { if (guidePanelHiddenByReader) { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); const hideGuide = localStorage.getItem('audiobookMakerHideGuide'); if (hideGuide !== 'true' && panel) { panel.classList.add('visible'); if (toggle) toggle.classList.remove('visible'); } guidePanelHiddenByReader = false; } } function onGuideDragStart(e) { if (e.target.closest('.guide-panel-btn') || e.target.closest('button')) return; const panel = document.getElementById('floatingGuidePanel'); if (!panel) return; guidePanelDragState.isDragging = true; const rect = panel.getBoundingClientRect(); guidePanelDragState.offsetX = e.clientX - rect.left; guidePanelDragState.offsetY = e.clientY - rect.top; panel.style.transition = 'none'; e.preventDefault(); } function onGuideDragMove(e) { if (!guidePanelDragState.isDragging) return; const panel = document.getElementById('floatingGuidePanel'); if (!panel) return; let newX = e.clientX - guidePanelDragState.offsetX; let newY = e.clientY - guidePanelDragState.offsetY; const pw = panel.offsetWidth, ph = panel.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - pw)); newY = Math.max(0, Math.min(newY, window.innerHeight - ph)); panel.style.right = 'auto'; panel.style.left = newX + 'px'; panel.style.top = newY + 'px'; e.preventDefault(); } function onGuideDragEnd(e) { if (!guidePanelDragState.isDragging) return; guidePanelDragState.isDragging = false; const panel = document.getElementById('floatingGuidePanel'); if (panel) { panel.style.transition = ''; localStorage.setItem('audiobookMakerGuidePos', JSON.stringify({ x: parseInt(panel.style.left) || 0, y: parseInt(panel.style.top) || 0 })); } } function onGuideTouchStart(e) { if (e.target.closest('.guide-panel-btn') || e.target.closest('button')) return; const panel = document.getElementById('floatingGuidePanel'); if (!panel) return; const touch = e.touches[0]; guidePanelDragState.isDragging = true; const rect = panel.getBoundingClientRect(); guidePanelDragState.offsetX = touch.clientX - rect.left; guidePanelDragState.offsetY = touch.clientY - rect.top; panel.style.transition = 'none'; e.preventDefault(); } function onGuideTouchMove(e) { if (!guidePanelDragState.isDragging) return; const panel = document.getElementById('floatingGuidePanel'); if (!panel) return; const touch = e.touches[0]; let newX = touch.clientX - guidePanelDragState.offsetX; let newY = touch.clientY - guidePanelDragState.offsetY; const pw = panel.offsetWidth, ph = panel.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - pw)); newY = Math.max(0, Math.min(newY, window.innerHeight - ph)); panel.style.right = 'auto'; panel.style.left = newX + 'px'; panel.style.top = newY + 'px'; e.preventDefault(); } function onGuideTouchEnd(e) { if (!guidePanelDragState.isDragging) return; guidePanelDragState.isDragging = false; const panel = document.getElementById('floatingGuidePanel'); if (panel) { panel.style.transition = ''; localStorage.setItem('audiobookMakerGuidePos', JSON.stringify({ x: parseInt(panel.style.left) || 0, y: parseInt(panel.style.top) || 0 })); } } // ============================================ // Workflow Progress Bar // ============================================ function updateWorkflowProgress(stage) { const step1 = document.getElementById('wpStep1'); const step2 = document.getElementById('wpStep2'); const step3 = document.getElementById('wpStep3'); const conn1 = document.getElementById('wpConn1'); const conn2 = document.getElementById('wpConn2'); if (!step1) return; currentWorkflowStage = stage; updateHeaderHelpButton(stage); [step1, step2, step3].forEach(s => s.classList.remove('completed', 'active')); [conn1, conn2].forEach(c => c.classList.remove('active')); switch (stage) { case 'upload': step1.classList.add('active'); break; case 'edit': step1.classList.add('completed'); conn1.classList.add('active'); step2.classList.add('active'); showGuidePanelOnEditor(); break; case 'audio-ready': step1.classList.add('completed'); conn1.classList.add('active'); step2.classList.add('completed'); conn2.classList.add('active'); step3.classList.add('active'); const badge = document.getElementById('readerTabBadge'); if (badge) badge.style.display = 'inline'; break; } } // ============================================ // Helpers // ============================================ function switchToEditorTab() { const editorTab = document.getElementById('editor-tab'); if (editorTab) { const tab = new bootstrap.Tab(editorTab); tab.show(); } } function startFromScratch() { document.getElementById('uploadSection').style.display = 'none'; document.getElementById('editorSection').style.display = 'block'; updateWorkflowProgress('edit'); const panel = document.getElementById('audiobookMakerPanel'); if (panel) panel.style.display = 'flex'; const sidebar = document.getElementById('documentOutlineSidebar'); if (sidebar) sidebar.style.display = 'block'; const editor = document.getElementById('markdownEditor'); if (editor && (typeof editorBlocks === 'undefined' || editorBlocks.length === 0)) { addBlock('paragraph', ''); repairAllNewBlockLines(); updatePanelUI(); } } function showLoader(text = 'Processing...', subtext = 'Please wait') { const overlay = document.getElementById('loadingOverlay'); if(overlay) { document.getElementById('loadingText').textContent = text; document.getElementById('loadingSubtext').textContent = subtext; overlay.classList.add('active'); } } function hideLoader() { const overlay = document.getElementById('loadingOverlay'); if(overlay) overlay.classList.remove('active'); } // ============================================ // Voice Management // ============================================ async function loadVoices() { try { const response = await fetch('/api/voices'); const data = await response.json(); voices = data.voices || []; console.log(`📢 Loaded ${voices.length} voices`); populatePanelVoiceSelect(); } catch (error) { console.error('Failed to load voices:', error); voices = [ { id: 'af_heart', name: 'Heart (US Female)' }, { id: 'am_adam', name: 'Adam (US Male)' } ]; populatePanelVoiceSelect(); } } function populatePanelVoiceSelect() { const select = document.getElementById('ampVoiceSelect'); if (!select) return; let currentVoice = 'af_heart'; if (typeof panelState !== 'undefined' && panelState.voice) { currentVoice = panelState.voice; } select.innerHTML = voices.map(v => `` ).join(''); } function getVoiceOptions(selectedVoice = 'af_heart') { return voices.map(v => `` ).join(''); } // ============================================ // Project Management // ============================================ async function saveProject() { const projectNameInput = document.getElementById('projectName'); if(!projectNameInput) return; const projectName = projectNameInput.value.trim(); if (!projectName) { alert('Please enter a project name'); return; } currentProject.name = projectName; const chapters = typeof collectEditorContent === 'function' ? collectEditorContent() : []; if (chapters.length === 0) { alert('No content to save. Add some blocks first.'); return; } showLoader('Saving Project...', 'Please wait'); try { let projectId = currentProject.id; if (!projectId) { const createResponse = await fetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: projectName }) }); const createData = await createResponse.json(); if (createData.error) { const listResponse = await fetch('/api/projects'); const listData = await listResponse.json(); const existing = listData.projects.find(p => p.name === projectName); if (existing) { projectId = existing.id; } else { throw new Error(createData.error); } } else { projectId = createData.project_id; } currentProject.id = projectId; } const saveResponse = await fetch(`/api/projects/${projectId}/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chapters }) }); const saveData = await saveResponse.json(); if (saveData.error) { throw new Error(saveData.error); } hideLoader(); showNotification('Project saved successfully!', 'success'); } catch (error) { hideLoader(); console.error('Save error:', error); alert('Failed to save project: ' + error.message); } } async function exportProject() { if (!currentProject.id) { await saveProject(); if (!currentProject.id) return; } showLoader('Exporting...', 'Creating ZIP file'); try { window.location.href = `/api/export/${currentProject.id}`; setTimeout(() => { hideLoader(); }, 2000); } catch (error) { hideLoader(); alert('Export failed: ' + error.message); } } async function openProjectArchive() { showLoader('Loading projects...'); try { const response = await fetch('/api/projects'); const data = await response.json(); allArchiveProjects = data.projects || []; const container = document.getElementById('projectList'); if(!container) return; if (allArchiveProjects.length === 0) { container.innerHTML = `
No saved projects yet