/** * 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

`; } else { container.innerHTML = allArchiveProjects.map(project => { const thumbHtml = project.thumbnail_data ? `thumbnail` : `
`; const publishBadge = project.is_published ? `Published` : ''; const canPublish = project.audio_count > 0; return `
${thumbHtml}
Edit
${escapeHtml(project.name)}
${publishBadge}
${project.chapter_count} sections • ${project.audio_count} audio blocks • ${project.view_count} views
${project.author ? `
${escapeHtml(project.author)}
` : ''}
${project.is_published ? `` : `` }
`; }).join(''); } hideLoader(); if(archiveModal) archiveModal.show(); } catch (error) { hideLoader(); alert('Failed to load projects: ' + error.message); } } // ============================================ // Rename // ============================================ function startEditProjectName(projectId) { document.getElementById(`project-info-${projectId}`).style.display = 'none'; document.getElementById(`project-actions-${projectId}`).style.display = 'none'; document.getElementById(`project-edit-${projectId}`).style.display = 'block'; document.getElementById(`project-edit-actions-${projectId}`).style.display = 'flex'; const input = document.getElementById(`edit-input-${projectId}`); input.focus(); input.select(); input.onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); saveProjectName(projectId); } else if (e.key === 'Escape') { cancelEditProjectName(projectId); } }; } function cancelEditProjectName(projectId) { document.getElementById(`project-info-${projectId}`).style.display = 'block'; document.getElementById(`project-actions-${projectId}`).style.display = 'flex'; document.getElementById(`project-edit-${projectId}`).style.display = 'none'; document.getElementById(`project-edit-actions-${projectId}`).style.display = 'none'; const textElement = document.getElementById(`project-name-text-${projectId}`); const input = document.getElementById(`edit-input-${projectId}`); if (textElement && input) { input.value = textElement.textContent; } } async function saveProjectName(projectId) { const input = document.getElementById(`edit-input-${projectId}`); const newName = input.value.trim(); if (!newName) { showNotification('Project name cannot be empty', 'warning'); return; } try { const response = await fetch(`/api/projects/${projectId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }) }); const data = await response.json(); if (data.error) { showNotification(data.error, 'error'); return; } const textEl = document.getElementById(`project-name-text-${projectId}`); if (textEl) textEl.textContent = newName; const cached = allArchiveProjects.find(p => p.id === projectId); if (cached) cached.name = newName; cancelEditProjectName(projectId); showNotification('Project renamed successfully', 'success'); if (currentProject.id === projectId) { currentProject.name = newName; const nameInput = document.getElementById('projectName'); if (nameInput) nameInput.value = newName; } } catch (error) { console.error(error); showNotification('Failed to rename project', 'error'); } } // ============================================ // Thumbnail Upload // ============================================ async function uploadThumbnail(projectId, inputEl) { const file = inputEl.files[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { showNotification('Image too large (max 5MB)', 'error'); return; } const formData = new FormData(); formData.append('file', file); showLoader('Uploading thumbnail...'); try { const resp = await fetch(`/api/projects/${projectId}/thumbnail`, { method: 'POST', body: formData }); const data = await resp.json(); hideLoader(); if (data.error) { showNotification(data.error, 'error'); return; } showNotification('Thumbnail updated', 'success'); openProjectArchive(); } catch (e) { hideLoader(); showNotification('Failed to upload thumbnail', 'error'); } } // ============================================ // Publishing // ============================================ function openPublishDialog(projectId) { publishingProjectId = projectId; const project = allArchiveProjects.find(p => p.id === projectId); if (!publishModal) { const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); publishModal = new bootstrap.Modal(document.getElementById('publishModal')); } document.getElementById('pub-name').value = project ? project.name : ''; document.getElementById('pub-author').value = project ? (project.author || '') : ''; document.getElementById('pub-description').value = project ? (project.description || '') : ''; document.getElementById('pub-category').value = project ? (project.category || '') : ''; publishModal.show(); } async function confirmPublish() { const author = document.getElementById('pub-author').value.trim(); const description = document.getElementById('pub-description').value.trim(); const category = document.getElementById('pub-category').value.trim(); try { const resp = await fetch(`/api/projects/${publishingProjectId}/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author, description, category }) }); const data = await resp.json(); if (data.error) { showNotification(data.error, 'error'); return; } publishModal.hide(); showNotification(data.message || 'Published!', 'success'); openProjectArchive(); } catch (e) { showNotification('Failed to publish', 'error'); } } async function unpublishProject(projectId) { if (!confirm('Unpublish this audiobook? It will no longer appear on the public library.')) return; try { const resp = await fetch(`/api/projects/${projectId}/unpublish`, { method: 'POST' }); const data = await resp.json(); if (data.error) { showNotification(data.error, 'error'); return; } showNotification('Unpublished', 'success'); openProjectArchive(); } catch (e) { showNotification('Failed to unpublish', 'error'); } } // ============================================ // Project Load / Delete - LAZY AUDIO LOADING // ============================================ async function loadProject(projectId) { showLoader('Loading project...', 'Fetching metadata'); if(archiveModal) archiveModal.hide(); try { // Step 1: Load lightweight metadata (no audio_data) const response = await fetch(`/api/projects/${projectId}`); const data = await response.json(); if (data.error) throw new Error(data.error); currentProject = { id: data.id, name: data.name, chapters: data.chapters }; document.getElementById('projectName').value = data.name; 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'; if (typeof renderProjectInEditor === 'function') { renderProjectInEditor(data); } // Step 2: Find blocks that need audio fetched const audioBlocks = []; for (const ch of data.chapters) { for (const bl of ch.blocks) { if (bl.has_audio && bl.block_type !== 'image') { audioBlocks.push(bl.id); } } } updateWorkflowProgress(audioBlocks.length > 0 ? 'audio-ready' : 'edit'); hideLoader(); if (audioBlocks.length === 0) { showNotification('Project loaded successfully!', 'success'); return; } // Step 3: Lazy-load audio in background, parallel batches showNotification(`Project loaded. Fetching ${audioBlocks.length} audio blocks in background...`, 'info'); loadAudioBlocksInBackground(projectId, audioBlocks); } catch (error) { hideLoader(); console.error('Load project error:', error); alert('Failed to load project: ' + error.message); } } async function loadAudioBlocksInBackground(projectId, blockIds) { const BATCH_SIZE = 5; let loaded = 0; let failed = 0; async function fetchOne(blockId) { try { const resp = await fetch(`/api/projects/${projectId}/audio/${blockId}`); const data = await resp.json(); if (data.error || !data.audio_data) { failed++; return; } // Update editorBlocks state by db_id const blockData = editorBlocks.find(b => b.db_id === blockId); if (blockData) { blockData.audio_data = data.audio_data; blockData.audio_format = data.audio_format; // Update DOM indicator (green dot) const blockEl = document.getElementById(blockData.id); if (blockEl) { const indicator = blockEl.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio loaded'; } } } loaded++; } catch (e) { failed++; console.warn(`Failed to load audio for block ${blockId}:`, e); } } for (let i = 0; i < blockIds.length; i += BATCH_SIZE) { const batch = blockIds.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(fetchOne)); } const msg = failed > 0 ? `Loaded ${loaded}/${blockIds.length} audio blocks (${failed} failed)` : `All ${loaded} audio blocks loaded ✓`; showNotification(msg, failed > 0 ? 'warning' : 'success'); if (typeof updatePanelUI === 'function') { updatePanelUI(); } } async function deleteProject(projectId) { if (!confirm('Are you sure you want to delete this project? This action cannot be undone.')) return; showLoader('Deleting...'); try { const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); const data = await response.json(); if (data.error) throw new Error(data.error); await openProjectArchive(); showNotification('Project deleted', 'success'); } catch (error) { hideLoader(); alert('Failed to delete project: ' + error.message); } } // ============================================ // TTS Text Editing // ============================================ function openTtsEditor(blockId, currentContent) { const plainText = stripMarkdown(currentContent); document.getElementById('ttsTextInput').value = plainText; document.getElementById('ttsBlockId').value = blockId; if(ttsEditModal) ttsEditModal.show(); } function saveTtsText() { const blockId = document.getElementById('ttsBlockId').value; const ttsText = document.getElementById('ttsTextInput').value; const block = document.querySelector(`[data-block-id="${blockId}"]`); if (block) { block.dataset.ttsText = ttsText; } if(ttsEditModal) ttsEditModal.hide(); showNotification('TTS text saved', 'success'); } // ============================================ // Utility Functions // ============================================ function escapeHtml(text) { if(!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function stripMarkdown(text) { if (!text) return ''; let result = text; result = result.replace(/^#{1,6}\s*/gm, ''); result = result.replace(/\*\*(.+?)\*\*/g, '$1'); result = result.replace(/\*(.+?)\*/g, '$1'); result = result.replace(/__(.+?)__/g, '$1'); result = result.replace(/_(.+?)_/g, '$1'); result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, ''); result = result.replace(/^>\s*/gm, ''); result = result.replace(/^\s*[-*+]\s+/gm, ''); result = result.replace(/^\s*\d+\.\s+/gm, ''); return result.trim(); } function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `alert alert-${type === 'success' ? 'success' : type === 'error' ? 'danger' : type === 'warning' ? 'warning' : 'info'} notification-toast`; notification.innerHTML = ` ${message} `; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 250px; animation: slideIn 0.3s ease; `; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000); } // ============================================ // Authentication // ============================================ let changePasswordModal = null; async function loadCurrentUser() { try { const response = await fetch('/api/auth/me'); const data = await response.json(); if (data.user) { const usernameEl = document.getElementById('headerUsername'); if (usernameEl) usernameEl.textContent = data.user.username; if (data.user.role === 'admin') { const adminItem = document.getElementById('adminMenuItem'); const adminDivider = document.getElementById('adminDivider'); if (adminItem) adminItem.style.display = 'block'; if (adminDivider) adminDivider.style.display = 'block'; } } } catch (error) { console.error('Failed to load user info:', error); } } async function handleLogout() { try { await fetch('/api/auth/logout', { method: 'POST' }); } catch(e) { /* ignore */ } window.location.href = '/login'; } function openChangePassword() { if (!changePasswordModal) { changePasswordModal = new bootstrap.Modal(document.getElementById('changePasswordModal')); } document.getElementById('currentPassword').value = ''; document.getElementById('newPassword').value = ''; document.getElementById('confirmPassword').value = ''; document.getElementById('changePasswordError').style.display = 'none'; changePasswordModal.show(); } async function submitChangePassword() { const currentPassword = document.getElementById('currentPassword').value; const newPassword = document.getElementById('newPassword').value; const confirmPassword = document.getElementById('confirmPassword').value; const errorDiv = document.getElementById('changePasswordError'); errorDiv.style.display = 'none'; if (!currentPassword || !newPassword) { errorDiv.textContent = 'All fields are required'; errorDiv.style.display = 'block'; return; } if (newPassword !== confirmPassword) { errorDiv.textContent = 'New passwords do not match'; errorDiv.style.display = 'block'; return; } if (newPassword.length < 4) { errorDiv.textContent = 'New password must be at least 4 characters'; errorDiv.style.display = 'block'; return; } try { const response = await fetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }) }); const data = await response.json(); if (data.error) { errorDiv.textContent = data.error; errorDiv.style.display = 'block'; return; } changePasswordModal.hide(); showNotification('Password changed successfully!', 'success'); } catch(e) { errorDiv.textContent = 'Network error. Please try again.'; errorDiv.style.display = 'block'; } } const notifStyle = document.createElement('style'); notifStyle.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(notifStyle);