/** * Audiobook Maker Pro v3.1 - Main Application * UPDATED: Dynamic header help button, floating guide panel, no hint bar * UPDATED: Hide guide panel by default when Interactive Reader tab is active */ // ============================================ // Global State // ============================================ let currentProject = { id: null, name: 'My Audiobook', chapters: [] }; let voices = []; let archiveModal = null; let ttsEditModal = null; let currentWorkflowStage = 'upload'; // ============================================ // Initialization // ============================================ document.addEventListener('DOMContentLoaded', function() { console.log('🎧 Audiobook Maker Pro v3.1 initializing...'); archiveModal = new bootstrap.Modal(document.getElementById('archiveModal')); ttsEditModal = new bootstrap.Modal(document.getElementById('ttsEditModal')); loadVoices(); initPdfHandler(); initMarkdownEditor(); setupEventListeners(); // Show welcome overlay for first-time users initWelcomeOverlay(); // Initialize workflow progress updateWorkflowProgress('upload'); // Initialize floating guide panel initFloatingGuidePanel(); // Load current user info loadCurrentUser(); // Ensure reader UI is hidden on startup 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(); } // Hide the guide panel when entering the reader hideGuidePanelForReader(); }); document.getElementById('editor-tab').addEventListener('shown.bs.tab', function() { if (typeof hideReaderUI === 'function') { hideReaderUI(); } // Restore the guide panel when returning to the editor 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; // Check if user previously hid the panel permanently const hideGuide = localStorage.getItem('audiobookMakerHideGuide'); if (hideGuide === 'true') { panel.classList.remove('visible'); if (toggle) toggle.classList.add('visible'); return; } // Check if user previously collapsed 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'); } } // Restore saved position 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 */ } } // Setup drag events (mouse) header.addEventListener('mousedown', onGuideDragStart); document.addEventListener('mousemove', onGuideDragMove); document.addEventListener('mouseup', onGuideDragEnd); // Touch support 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'); } } // ============================================ // Guide Panel: Reader Tab Visibility // ============================================ let guidePanelHiddenByReader = false; function hideGuidePanelForReader() { const panel = document.getElementById('floatingGuidePanel'); const toggle = document.getElementById('floatingGuideToggle'); // If the panel is currently visible, hide it and remember we did so if (panel && panel.classList.contains('visible')) { panel.classList.remove('visible'); guidePanelHiddenByReader = true; } else { guidePanelHiddenByReader = false; } // Always show the toggle button so the user CAN show it manually if they want if (toggle) toggle.classList.add('visible'); } function restoreGuidePanelForEditor() { // Only restore if we were the ones who hid it 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; } } // --- Drag Logic (Mouse) --- 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 panelWidth = panel.offsetWidth; const panelHeight = panel.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - panelWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - panelHeight)); 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 })); } } // --- Drag Logic (Touch) --- 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 panelWidth = panel.offsetWidth; const panelHeight = panel.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - panelWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - panelHeight)); 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; // Track current stage for help button currentWorkflowStage = stage; updateHeaderHelpButton(stage); // Reset all [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'); // Show the floating guide when entering editor 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'); // Show badge on reader tab const badge = document.getElementById('readerTabBadge'); if (badge) badge.style.display = 'inline'; break; } } // ============================================ // Helper: Switch to Editor Tab // ============================================ function switchToEditorTab() { const editorTab = document.getElementById('editor-tab'); if (editorTab) { const tab = new bootstrap.Tab(editorTab); tab.show(); } } // ============================================ // Helper: Start from scratch // ============================================ function startFromScratch() { document.getElementById('uploadSection').style.display = 'none'; document.getElementById('editorSection').style.display = 'block'; updateWorkflowProgress('edit'); // Click the editor to trigger first chapter creation const editor = document.getElementById('markdownEditor'); if (editor && editorBlocks.length === 0) { addChapterMarker(1); } } // ============================================ // Loading Overlay // ============================================ function showLoader(text = 'Processing...', subtext = 'Please wait') { const overlay = document.getElementById('loadingOverlay'); document.getElementById('loadingText').textContent = text; document.getElementById('loadingSubtext').textContent = subtext; overlay.classList.add('active'); } function hideLoader() { document.getElementById('loadingOverlay').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`); } catch (error) { console.error('Failed to load voices:', error); voices = [ { id: 'af_heart', name: 'Heart (US Female)' }, { id: 'am_adam', name: 'Adam (US Male)' } ]; } } function getVoiceOptions(selectedVoice = 'af_heart') { return voices.map(v => `` ).join(''); } // ============================================ // Project Management // ============================================ async function saveProject() { const projectName = document.getElementById('projectName').value.trim(); if (!projectName) { alert('Please enter a project name'); return; } currentProject.name = projectName; const chapters = collectEditorContent(); if (chapters.length === 0) { alert('No content to save. Add some chapters and 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(); const container = document.getElementById('projectList'); if (data.projects.length === 0) { container.innerHTML = `
No saved projects yet