// ======================================================================= // Notes Application - Main Orchestrator & Router (Native ES Module) // Zero Bundlers, Zero npm, Native browser ES6 modules // ======================================================================= import { api } from './modules/api.js'; import { initTheme, openModal, closeModal, showToast, playTextSpeech, switchView } from './modules/ui.js'; import { initDictionary, loadInitialData } from './modules/dictionary.js'; import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js'; import { initReader, loadBookshelf, openBook, openWordPopup, extractContextSentence } from './modules/reader.js'; import { initCefr, loadCefrOverview, startCefrQuiz } from './modules/cefr.js'; import { initGrammar, loadGrammarUnits, openGrammarUnit } from './modules/grammar.js'; import { initLessonModule, openLessonConfigurator } from './modules/lesson.js'; import { initFeedbackWidget } from './modules/feedback.js'; let currentUser = null; let isRouting = false; // ----------------------------------------------------------------------- // Unified Navigation & Drawer Active State // ----------------------------------------------------------------------- export function updateActiveNavState(viewName) { // 1. Mobile bottom tab bar document.querySelectorAll('.mobile-tab-bar .tab-item').forEach(btn => { btn.classList.remove('active'); }); if (viewName === 'dict' || viewName === 'dictionary') { const t = document.getElementById('tab-nav-dict'); if (t) t.classList.add('active'); } else if (viewName === 'srs') { const t = document.getElementById('tab-nav-srs'); if (t) t.classList.add('active'); } else if (viewName === 'reader') { const t = document.getElementById('tab-nav-reader'); if (t) t.classList.add('active'); } // 2. Drawer nav items document.querySelectorAll('.drawer-nav-item').forEach(btn => { btn.classList.remove('active'); }); const drawerMap = { 'dict': 'drawer-nav-dict', 'dictionary': 'drawer-nav-dict', 'srs': 'drawer-nav-srs', 'reader': 'drawer-nav-reader', 'cefr': 'drawer-nav-cefr', 'grammar': 'drawer-nav-grammar', 'lesson': 'drawer-nav-lesson', }; const targetId = drawerMap[viewName]; if (targetId) { const dBtn = document.getElementById(targetId); if (dBtn) dBtn.classList.add('active'); } } export function updateMobileActiveTab(tabName) { updateActiveNavState(tabName); } // ----------------------------------------------------------------------- // Immediate Pre-flight Routing (Zero-flicker view restoration on F5) // ----------------------------------------------------------------------- export function applyInitialViewPreflight() { let hash = window.location.hash || ''; if (!hash || hash === '#') { try { const lastView = localStorage.getItem('notes_active_view'); const lastBookId = localStorage.getItem('notes_active_book_id'); if (lastView === 'reader-view') { hash = lastBookId ? `#reader/book/${lastBookId}` : '#reader'; } else if (lastView === 'srs-view') { hash = '#srs'; } else if (lastView === 'cefr-view') { hash = '#cefr'; } else if (lastView === 'grammar-view') { hash = '#grammar'; } else if (lastView === 'lesson-view') { hash = '#lesson'; } } catch (e) {} } const bookshelfContainer = document.getElementById('bookshelf-container'); const readerInterface = document.getElementById('reader-interface'); const readerBookTitle = document.getElementById('reader-book-title'); if (hash.startsWith('#reader') || hash === '#bookshelf') { switchView('reader-view'); updateActiveNavState('reader'); if (hash.includes('/book/')) { if (bookshelfContainer) bookshelfContainer.classList.add('hidden'); if (readerInterface) readerInterface.classList.remove('hidden'); if (readerBookTitle) readerBookTitle.textContent = 'Загрузка книги...'; } } else if (hash.startsWith('#srs')) { switchView('srs-view'); updateActiveNavState('srs'); } else if (hash.startsWith('#cefr')) { switchView('cefr-view'); updateActiveNavState('cefr'); } else if (hash.startsWith('#grammar')) { switchView('grammar-view'); updateActiveNavState('grammar'); } else if (hash.startsWith('#lesson')) { switchView('lesson-view'); updateActiveNavState('lesson'); } } // ----------------------------------------------------------------------- // View Transitions // ----------------------------------------------------------------------- export function openDictionaryView(updateHash = true) { if (updateHash && window.location.hash !== '#dictionary') { window.location.hash = '#dictionary'; return; } switchView('dictionary-view'); updateActiveNavState('dict'); try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {} loadInitialData(); } export function openSrsView(tagFilter = null, updateHash = true) { const targetHash = tagFilter ? `#srs/${encodeURIComponent(tagFilter)}` : '#srs'; if (updateHash && window.location.hash !== targetHash) { window.location.hash = targetHash; return; } switchView('srs-view'); updateActiveNavState('srs'); try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {} startSrsTrainingSession(false, tagFilter); } export function closeSrsView() { openDictionaryView(true); } export function openBookshelfView(updateHash = true) { if (updateHash && window.location.hash !== '#reader') { window.location.hash = '#reader'; return; } switchView('reader-view'); const bookshelfContainer = document.getElementById('bookshelf-container'); const readerInterface = document.getElementById('reader-interface'); if (bookshelfContainer) bookshelfContainer.classList.remove('hidden'); if (readerInterface) readerInterface.classList.add('hidden'); updateActiveNavState('reader'); try { localStorage.setItem('notes_active_view', 'reader-view'); localStorage.removeItem('notes_active_book_id'); } catch (e) {} loadBookshelf(); } export function openReaderView(updateHash = true) { const lastBookId = localStorage.getItem('notes_active_book_id'); if (lastBookId) { window.location.hash = `#reader/book/${lastBookId}`; } else { openBookshelfView(updateHash); } } export function openCefrView(level = null, mode = 'all', updateHash = true) { const targetHash = level ? (mode === 'mistakes' ? `#cefr/${encodeURIComponent(level)}/mistakes` : `#cefr/${encodeURIComponent(level)}`) : '#cefr'; if (updateHash && window.location.hash !== targetHash) { window.location.hash = targetHash; return; } switchView('cefr-view'); updateActiveNavState('cefr'); try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {} if (level) { startCefrQuiz(level, mode); } else { loadCefrOverview(); } } export function openGrammarView(unitId = null, updateHash = true) { const targetHash = unitId ? `#grammar/unit/${unitId}` : '#grammar'; if (updateHash && window.location.hash !== targetHash) { window.location.hash = targetHash; return; } switchView('grammar-view'); updateActiveNavState('grammar'); try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {} if (unitId) { openGrammarUnit(unitId); } else { loadGrammarUnits(); } } export function openLessonView(presetId = null, updateHash = true) { const targetHash = presetId ? `#lesson/${presetId}` : '#lesson'; if (updateHash && !window.location.hash.startsWith('#lesson')) { window.location.hash = targetHash; return; } switchView('lesson-view'); updateActiveNavState('lesson'); try { localStorage.setItem('notes_active_view', 'lesson-view'); } catch (e) {} openLessonConfigurator(presetId); } // ----------------------------------------------------------------------- // Route Handler // ----------------------------------------------------------------------- export async function handleRouting() { if (isRouting) return; isRouting = true; try { let hash = window.location.hash || ''; if (!hash || hash === '#') { try { const lastView = localStorage.getItem('notes_active_view'); const lastBookId = localStorage.getItem('notes_active_book_id'); if (lastView === 'reader-view') { hash = lastBookId ? `#reader/book/${lastBookId}` : '#reader'; window.location.hash = hash; return; } else if (lastView === 'srs-view') { hash = '#srs'; window.location.hash = hash; return; } else if (lastView === 'cefr-view') { hash = '#cefr'; window.location.hash = hash; return; } else if (lastView === 'grammar-view') { hash = '#grammar'; window.location.hash = hash; return; } else if (lastView === 'lesson-view') { hash = '#lesson'; window.location.hash = hash; return; } else { hash = '#dictionary'; } } catch (e) { hash = '#dictionary'; } } const cleanHash = hash.replace(/^#\/?/, ''); if (!cleanHash || cleanHash === 'dictionary') { openDictionaryView(false); } else if (cleanHash.startsWith('srs')) { const parts = cleanHash.split('/'); const tag = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('/')) : null; openSrsView(tag, false); } else if (cleanHash.startsWith('cefr')) { const parts = cleanHash.split('/'); const level = parts[1] || null; const mode = parts[2] === 'mistakes' ? 'mistakes' : 'all'; openCefrView(level, mode, false); } else if (cleanHash.startsWith('grammar')) { const parts = cleanHash.split('/'); if (parts[1] === 'unit' && parts[2]) { const unitId = parseInt(parts[2], 10); openGrammarView(unitId, false); } else { openGrammarView(null, false); } } else if (cleanHash.startsWith('reader') || cleanHash === 'bookshelf') { const parts = cleanHash.split('/'); if (parts[1] === 'book' && parts[2]) { const bookId = parseInt(parts[2], 10); const chIdx = (parts[3] === 'ch' && parts[4] !== undefined) ? parseInt(parts[4], 10) : null; await openBook(bookId, chIdx, false); } else { openBookshelfView(false); } } else if (cleanHash.startsWith('lesson')) { const parts = cleanHash.split('/'); const presetId = parts[1] || null; openLessonView(presetId, false); } else { openDictionaryView(false); } } finally { isRouting = false; } } // ----------------------------------------------------------------------- // Session & Auth // ----------------------------------------------------------------------- export async function checkAuth() { try { const resp = await api.auth.me(); if (resp.ok) { const data = await resp.json(); currentUser = data.user; showApp(); await handleRouting(); loadInitialData(); } else { showLogin(); } } catch (e) { showLogin(); } } export function showLogin() { const loginOverlay = document.getElementById('login-overlay'); const appShell = document.getElementById('app-shell'); const loginUsername = document.getElementById('login-username'); if (loginOverlay) loginOverlay.classList.remove('hidden'); if (appShell) appShell.classList.add('hidden'); if (loginUsername) loginUsername.focus(); } export function showApp() { const loginOverlay = document.getElementById('login-overlay'); const appShell = document.getElementById('app-shell'); const userDisplayName = document.getElementById('user-display-name'); const drawerUserName = document.getElementById('drawer-user-name'); const mobileUserName = document.getElementById('mobile-user-name'); if (loginOverlay) loginOverlay.classList.add('hidden'); if (appShell) appShell.classList.remove('hidden'); const name = currentUser ? currentUser.username : 'user'; if (userDisplayName) userDisplayName.textContent = name; if (drawerUserName) drawerUserName.textContent = name; if (mobileUserName) mobileUserName.textContent = name; } // ----------------------------------------------------------------------- // Unified Navigation Drawer Controller (Desktop & Mobile) // ----------------------------------------------------------------------- export function initNavigationDrawer() { const drawerBackdrop = document.getElementById('app-drawer-backdrop'); const drawer = document.getElementById('app-drawer'); const drawerToggleBtn = document.getElementById('drawer-toggle-btn'); const drawerCloseBtn = document.getElementById('app-drawer-close-btn'); const navUserProfileBtn = document.getElementById('nav-user-profile-btn'); const tabMenu = document.getElementById('tab-nav-menu'); // Bottom tab items const tabDict = document.getElementById('tab-nav-dict'); const tabSrs = document.getElementById('tab-nav-srs'); const tabReader = document.getElementById('tab-nav-reader'); // Drawer nav buttons const drawerNavDict = document.getElementById('drawer-nav-dict'); const drawerNavSrs = document.getElementById('drawer-nav-srs'); const drawerNavReader = document.getElementById('drawer-nav-reader'); const drawerNavCefr = document.getElementById('drawer-nav-cefr'); const drawerNavGrammar = document.getElementById('drawer-nav-grammar'); const drawerNavStats = document.getElementById('drawer-nav-stats'); const drawerNavExport = document.getElementById('drawer-nav-export'); const drawerNavPwd = document.getElementById('drawer-nav-pwd'); const drawerNavTheme = document.getElementById('drawer-nav-theme'); const drawerNavLogout = document.getElementById('drawer-nav-logout'); const exportModal = document.getElementById('export-modal'); const changePasswordModal = document.getElementById('change-password-modal'); const themeToggle = document.getElementById('theme-toggle'); const logoutBtn = document.getElementById('logout-btn'); function openDrawer() { if (!drawer || !drawerBackdrop) return; const drawerUserName = document.getElementById('drawer-user-name'); if (drawerUserName && currentUser) { drawerUserName.textContent = currentUser.username; } drawerBackdrop.classList.remove('hidden'); // Force reflow for animation void drawerBackdrop.offsetWidth; drawerBackdrop.classList.add('active'); drawer.classList.add('active'); document.body.classList.add('drawer-open'); } function closeDrawer() { if (!drawer || !drawerBackdrop) return; drawer.classList.remove('active'); drawerBackdrop.classList.remove('active'); document.body.classList.remove('drawer-open'); setTimeout(() => { if (!drawer.classList.contains('active')) { drawerBackdrop.classList.add('hidden'); } }, 280); } // Opening Triggers if (drawerToggleBtn) drawerToggleBtn.addEventListener('click', openDrawer); if (navUserProfileBtn) navUserProfileBtn.addEventListener('click', openDrawer); if (tabMenu) tabMenu.addEventListener('click', openDrawer); // Closing Triggers if (drawerCloseBtn) drawerCloseBtn.addEventListener('click', closeDrawer); if (drawerBackdrop) drawerBackdrop.addEventListener('click', closeDrawer); // Global Escape Key & Modal Dismiss Support document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { // 1. If navigation drawer is open, close it if (drawer && drawer.classList.contains('active')) { closeDrawer(); return; } // 2. Close any open modal backdrop (topmost visible modal) const openModals = Array.from(document.querySelectorAll('.modal-backdrop:not(.hidden)')); if (openModals.length > 0) { const topModal = openModals[openModals.length - 1]; topModal.classList.add('hidden'); } } }); // Dismiss modals when clicking on dark backdrop outside modal window document.addEventListener('click', (e) => { if (e.target && e.target.classList && e.target.classList.contains('modal-backdrop')) { e.target.classList.add('hidden'); } }); // ----------------------------------------------------------------------- // Global Text Selection Lookup & Add-to-Dictionary (Anywhere on Page) // ----------------------------------------------------------------------- function handleGlobalTextSelection(e) { if (e && e.target && e.target.closest('#reader-word-popup, button, input, textarea, select, .modal-window, #feedback-widget, .icon-btn, .opt-btn, .tab-item, .drawer-nav-item')) { return; } const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const raw = selection.toString().trim(); if (!raw || raw.length < 2 || raw.length > 50) return; // Clean punctuation / quotes const clean = raw.replace(/^['"“«`\s\-_.,;:!?()[\]{}]+|['"”»`\s\-_.,;:!?()[\]{}]+$/g, '').trim(); // Require Latin characters (English vocabulary) if (!clean || !/^[A-Za-z\s'-]+$/.test(clean)) return; if (selection.rangeCount === 0) return; const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return; const containerElem = range.startContainer?.nodeType === 1 ? range.startContainer : range.startContainer?.parentElement; // Avoid re-triggering if already inside reader word span (which handles its own click) if (containerElem?.closest('.reader-word')) return; const fullText = containerElem?.innerText || containerElem?.textContent || ''; const contextSentence = extractContextSentence(fullText, clean); const virtualTarget = { getBoundingClientRect: () => range.getBoundingClientRect(), isConnected: true }; openWordPopup(clean, contextSentence, virtualTarget, fullText); } document.addEventListener('mouseup', handleGlobalTextSelection); document.addEventListener('touchend', (e) => { setTimeout(() => handleGlobalTextSelection(e), 150); }); // Touch Swipe Left to Dismiss (Mobile & Tablet) let touchStartX = 0; let touchCurrentX = 0; if (drawer) { drawer.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; touchCurrentX = touchStartX; }, { passive: true }); drawer.addEventListener('touchmove', (e) => { touchCurrentX = e.touches[0].clientX; }, { passive: true }); drawer.addEventListener('touchend', () => { if (touchStartX - touchCurrentX > 60 && touchCurrentX > 0) { closeDrawer(); } touchStartX = 0; touchCurrentX = 0; }); } // Bottom Tab Items if (tabDict) tabDict.addEventListener('click', () => { window.location.hash = '#dictionary'; }); if (tabSrs) tabSrs.addEventListener('click', () => { window.location.hash = '#srs'; }); if (tabReader) tabReader.addEventListener('click', () => { window.location.hash = '#reader'; }); // Drawer Navigation Actions if (drawerNavDict) { drawerNavDict.addEventListener('click', () => { closeDrawer(); window.location.hash = '#dictionary'; }); } if (drawerNavSrs) { drawerNavSrs.addEventListener('click', () => { closeDrawer(); window.location.hash = '#srs'; }); } if (drawerNavReader) { drawerNavReader.addEventListener('click', () => { closeDrawer(); window.location.hash = '#reader'; }); } if (drawerNavCefr) { drawerNavCefr.addEventListener('click', () => { closeDrawer(); window.location.hash = '#cefr'; }); } if (drawerNavGrammar) { drawerNavGrammar.addEventListener('click', () => { closeDrawer(); window.location.hash = '#grammar'; }); } const drawerNavLesson = document.getElementById('drawer-nav-lesson'); if (drawerNavLesson) { drawerNavLesson.addEventListener('click', () => { closeDrawer(); window.location.hash = '#lesson'; }); } if (drawerNavStats) { drawerNavStats.addEventListener('click', () => { closeDrawer(); openStatsDashboard(); }); } if (drawerNavExport && exportModal) { drawerNavExport.addEventListener('click', () => { closeDrawer(); openModal(exportModal); }); } if (drawerNavPwd && changePasswordModal) { drawerNavPwd.addEventListener('click', () => { closeDrawer(); openModal(changePasswordModal); }); } if (drawerNavTheme && themeToggle) { drawerNavTheme.addEventListener('click', () => { themeToggle.click(); }); } const drawerNavVoiceSettings = document.getElementById('drawer-nav-voice-settings'); const voiceSettingsModal = document.getElementById('voice-settings-modal'); if (drawerNavVoiceSettings && voiceSettingsModal) { drawerNavVoiceSettings.addEventListener('click', () => { closeDrawer(); openVoiceSettingsModal(); }); } if (drawerNavLogout && logoutBtn) { drawerNavLogout.addEventListener('click', () => { closeDrawer(); logoutBtn.click(); }); } } // ----------------------------------------------------------------------- // Voice Settings Modal Controller (TTS) // ----------------------------------------------------------------------- function openVoiceSettingsModal() { const voiceSettingsModal = document.getElementById('voice-settings-modal'); if (!voiceSettingsModal) return; const currentRu = localStorage.getItem('notes_tts_voice_ru') || 'ru_RU-irina'; const currentEn = localStorage.getItem('notes_tts_voice_en') || 'en_US-lessac'; const ruInput = voiceSettingsModal.querySelector(`input[name="tts_voice_ru"][value="${currentRu}"]`); if (ruInput) ruInput.checked = true; const enInput = voiceSettingsModal.querySelector(`input[name="tts_voice_en"][value="${currentEn}"]`); if (enInput) enInput.checked = true; openModal(voiceSettingsModal); } function initVoiceSettingsModal() { const voiceSettingsModal = document.getElementById('voice-settings-modal'); const voiceSettingsCloseBtn = document.getElementById('voice-settings-close-btn'); const voiceSettingsSaveBtn = document.getElementById('voice-settings-save-btn'); if (voiceSettingsCloseBtn && voiceSettingsModal) { voiceSettingsCloseBtn.addEventListener('click', () => closeModal(voiceSettingsModal)); } if (voiceSettingsSaveBtn && voiceSettingsModal) { voiceSettingsSaveBtn.addEventListener('click', () => { const ruVal = voiceSettingsModal.querySelector('input[name="tts_voice_ru"]:checked')?.value || 'ru_RU-irina'; const enVal = voiceSettingsModal.querySelector('input[name="tts_voice_en"]:checked')?.value || 'en_US-lessac'; localStorage.setItem('notes_tts_voice_ru', ruVal); localStorage.setItem('notes_tts_voice_en', enVal); // Sync with listening selector if on screen const listeningVoiceSelect = document.getElementById('listening-voice-select'); if (listeningVoiceSelect) { listeningVoiceSelect.value = enVal; } closeModal(voiceSettingsModal); showToast('✅ Голоса дикторов сохранены!', 'success'); }); } // Voice test buttons if (voiceSettingsModal) { voiceSettingsModal.querySelectorAll('.voice-test-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const lang = btn.dataset.lang || 'en'; const voice = btn.dataset.voice; const text = btn.dataset.text || 'Test voice'; playTextSpeech(text, lang, 1.0, voice); }); }); } } // ----------------------------------------------------------------------- // Password Change Modal // ----------------------------------------------------------------------- function initChangePasswordModal() { const changePasswordModal = document.getElementById('change-password-modal'); const closeChangePwdBtn = document.getElementById('close-change-pwd-btn'); const changePwdCancelBtn = document.getElementById('change-pwd-cancel-btn'); const changePwdOpenBtn = document.getElementById('change-pwd-open-btn'); const changePasswordForm = document.getElementById('change-password-form'); const currentPasswordInput = document.getElementById('current-password-input'); const newPasswordInput = document.getElementById('new-password-input'); const confirmPasswordInput = document.getElementById('confirm-password-input'); const changePwdError = document.getElementById('change-pwd-error'); const changePwdSuccess = document.getElementById('change-pwd-success'); if (changePwdOpenBtn && changePasswordModal) { changePwdOpenBtn.addEventListener('click', () => { if (changePwdError) changePwdError.classList.add('hidden'); if (changePwdSuccess) changePwdSuccess.classList.add('hidden'); if (currentPasswordInput) currentPasswordInput.value = ''; if (newPasswordInput) newPasswordInput.value = ''; if (confirmPasswordInput) confirmPasswordInput.value = ''; openModal(changePasswordModal); if (currentPasswordInput) currentPasswordInput.focus(); }); } if (closeChangePwdBtn && changePasswordModal) { closeChangePwdBtn.addEventListener('click', () => closeModal(changePasswordModal)); } if (changePwdCancelBtn && changePasswordModal) { changePwdCancelBtn.addEventListener('click', () => closeModal(changePasswordModal)); } if (changePasswordForm) { changePasswordForm.addEventListener('submit', async (e) => { e.preventDefault(); if (changePwdError) changePwdError.classList.add('hidden'); if (changePwdSuccess) changePwdSuccess.classList.add('hidden'); const currentPwd = currentPasswordInput ? currentPasswordInput.value : ''; const newPwd = newPasswordInput ? newPasswordInput.value : ''; const confirmPwd = confirmPasswordInput ? confirmPasswordInput.value : ''; if (newPwd !== confirmPwd) { if (changePwdError) { changePwdError.textContent = 'Новые пароли не совпадают'; changePwdError.classList.remove('hidden'); } return; } try { const resp = await api.auth.changePassword(currentPwd, newPwd); const data = await resp.json(); if (resp.ok) { if (changePwdSuccess) { changePwdSuccess.textContent = 'Пароль успешно обновлен!'; changePwdSuccess.classList.remove('hidden'); } setTimeout(() => { if (changePasswordModal) closeModal(changePasswordModal); }, 1200); } else { if (changePwdError) { changePwdError.textContent = data.detail || 'Ошибка смены пароля'; changePwdError.classList.remove('hidden'); } } } catch (err) { if (changePwdError) { changePwdError.textContent = 'Ошибка сетевого соединения'; changePwdError.classList.remove('hidden'); } } }); } } // ----------------------------------------------------------------------- // Initialization & Entry Point // ----------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { // 1. PWA Service Worker Registration if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/' }) .then(reg => console.log('Notes PWA ServiceWorker active:', reg.scope)) .catch(err => console.log('Notes ServiceWorker failed:', err)); } // 2. Pre-flight routing check to avoid DOM flicker on reload applyInitialViewPreflight(); // 3. UI theme initialization initTheme(); // 4. Initialize Feature Modules initDictionary(); initSrs(); initReader(); initCefr(); initGrammar(); initLessonModule(); initFeedbackWidget(); // 5. Global Navigation & Header Actions const navBrandHome = document.getElementById('nav-brand-home'); const navReaderBtn = document.getElementById('nav-reader-btn'); const navCefrBtn = document.getElementById('nav-cefr-btn'); const navGrammarBtn = document.getElementById('nav-grammar-btn'); const srsTrainerBtn = document.getElementById('srs-trainer-btn'); const srsJumpCefrBtn = document.getElementById('srs-jump-cefr-btn'); const loginForm = document.getElementById('login-form'); const loginUsername = document.getElementById('login-username'); const loginPassword = document.getElementById('login-password'); const loginError = document.getElementById('login-error'); const logoutBtn = document.getElementById('logout-btn'); if (navBrandHome) { navBrandHome.addEventListener('click', () => { window.location.hash = '#dictionary'; }); } if (navReaderBtn) { navReaderBtn.addEventListener('click', () => { window.location.hash = '#reader'; }); } if (navCefrBtn) { navCefrBtn.addEventListener('click', () => { window.location.hash = '#cefr'; }); } if (navGrammarBtn) { navGrammarBtn.addEventListener('click', () => { window.location.hash = '#grammar'; }); } if (srsTrainerBtn) { srsTrainerBtn.addEventListener('click', () => { window.location.hash = '#srs'; }); } if (srsJumpCefrBtn) { srsJumpCefrBtn.addEventListener('click', () => { window.location.hash = '#cefr'; }); } // 6. Navigation Drawer & Mobile Tab Bar initNavigationDrawer(); // 7. Change Password Modal initChangePasswordModal(); // 7b. Voice Settings Modal (TTS) initVoiceSettingsModal(); // 8. Hash Change Listener window.addEventListener('hashchange', handleRouting); // 9. Auth Actions if (loginForm) { loginForm.addEventListener('submit', async (e) => { e.preventDefault(); if (loginError) loginError.classList.add('hidden'); try { const resp = await api.auth.login( loginUsername ? loginUsername.value.trim() : '', loginPassword ? loginPassword.value : '' ); const data = await resp.json(); if (resp.ok && data.status === 'ok') { if (data.token) { localStorage.setItem('notes_token', data.token); } currentUser = data.user; showApp(); await handleRouting(); loadInitialData(); } else { if (loginError) { loginError.textContent = data.detail || 'Неверный логин или пароль'; loginError.classList.remove('hidden'); } } } catch (err) { if (loginError) { loginError.textContent = 'Ошибка сетевого соединения'; loginError.classList.remove('hidden'); } } }); } if (logoutBtn) { logoutBtn.addEventListener('click', async () => { localStorage.removeItem('notes_token'); localStorage.removeItem('notes_active_book_id'); try { await api.auth.logout(); } catch (e) {} currentUser = null; window.location.hash = ''; window.location.reload(); }); } // 10. Check Session Auth & Kickoff checkAuth(); });