// ======================================================================= // 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 } from './modules/ui.js'; import { initDictionary, loadInitialData } from './modules/dictionary.js'; import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js'; import { initReader, loadBookshelf, openBook } from './modules/reader.js'; import { initCefr, loadCefrOverview, startCefrQuiz } from './modules/cefr.js'; import { initGrammar, loadGrammarUnits, openGrammarUnit } from './modules/grammar.js'; let currentUser = null; let isRouting = false; // ----------------------------------------------------------------------- // Mobile Navigation Active Tab // ----------------------------------------------------------------------- export function updateMobileActiveTab(tabName) { document.querySelectorAll('.mobile-tab-bar .tab-item').forEach(btn => { btn.classList.remove('active'); }); if (!tabName) return; const target = document.getElementById(`tab-nav-${tabName}`); if (target) target.classList.add('active'); } // ----------------------------------------------------------------------- // 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'; } } catch (e) {} } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); 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') { if (dictionaryView) dictionaryView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (readerView) readerView.classList.remove('hidden'); updateMobileActiveTab('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')) { if (dictionaryView) dictionaryView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (srsView) srsView.classList.remove('hidden'); updateMobileActiveTab('srs'); } else if (hash.startsWith('#cefr')) { if (dictionaryView) dictionaryView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (cefrView) cefrView.classList.remove('hidden'); updateMobileActiveTab(''); } else if (hash.startsWith('#grammar')) { if (dictionaryView) dictionaryView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.remove('hidden'); updateMobileActiveTab(''); } } // ----------------------------------------------------------------------- // View Transitions // ----------------------------------------------------------------------- export function openDictionaryView(updateHash = true) { if (updateHash && window.location.hash !== '#dictionary') { window.location.hash = '#dictionary'; return; } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); if (srsView) srsView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (dictionaryView) dictionaryView.classList.remove('hidden'); updateMobileActiveTab('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; } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); if (dictionaryView) dictionaryView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (srsView) srsView.classList.remove('hidden'); updateMobileActiveTab('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; } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); const bookshelfContainer = document.getElementById('bookshelf-container'); const readerInterface = document.getElementById('reader-interface'); if (dictionaryView) dictionaryView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (readerView) readerView.classList.remove('hidden'); if (bookshelfContainer) bookshelfContainer.classList.remove('hidden'); if (readerInterface) readerInterface.classList.add('hidden'); updateMobileActiveTab('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; } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); if (dictionaryView) dictionaryView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (grammarView) grammarView.classList.add('hidden'); if (cefrView) cefrView.classList.remove('hidden'); updateMobileActiveTab(''); 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; } const dictionaryView = document.getElementById('dictionary-view'); const srsView = document.getElementById('srs-view'); const readerView = document.getElementById('reader-view'); const cefrView = document.getElementById('cefr-view'); const grammarView = document.getElementById('grammar-view'); if (dictionaryView) dictionaryView.classList.add('hidden'); if (srsView) srsView.classList.add('hidden'); if (readerView) readerView.classList.add('hidden'); if (cefrView) cefrView.classList.add('hidden'); if (grammarView) grammarView.classList.remove('hidden'); updateMobileActiveTab(''); try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {} if (unitId) { openGrammarUnit(unitId); } else { loadGrammarUnits(); } } // ----------------------------------------------------------------------- // 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 { 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 { 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 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 (mobileUserName) mobileUserName.textContent = name; } // ----------------------------------------------------------------------- // Mobile Navigation & Drawer // ----------------------------------------------------------------------- function initMobileNavigation() { const tabDict = document.getElementById('tab-nav-dict'); const tabSrs = document.getElementById('tab-nav-srs'); const tabReader = document.getElementById('tab-nav-reader'); const tabStats = document.getElementById('tab-nav-stats'); const tabMenu = document.getElementById('tab-nav-menu'); const mobileMenuModal = document.getElementById('mobile-menu-modal'); const mobileMenuToggleBtn = document.getElementById('mobile-menu-toggle-btn'); const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn'); const mobileStatsBtn = document.getElementById('mobile-stats-btn'); const mobileExportBtn = document.getElementById('mobile-export-btn'); const mobilePwdBtn = document.getElementById('mobile-pwd-btn'); const mobileThemeBtn = document.getElementById('mobile-theme-btn'); const mobileLogoutBtn = document.getElementById('mobile-logout-btn'); 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'); 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'; }); if (tabStats) { tabStats.addEventListener('click', () => { openStatsDashboard(); }); } function openMobileMenu() { if (mobileMenuModal) { const mobileUserName = document.getElementById('mobile-user-name'); if (mobileUserName && currentUser) { mobileUserName.textContent = currentUser.username; } openModal(mobileMenuModal); } } function closeMobileMenu() { if (mobileMenuModal) { closeModal(mobileMenuModal); } } if (tabMenu) tabMenu.addEventListener('click', openMobileMenu); if (mobileMenuToggleBtn) mobileMenuToggleBtn.addEventListener('click', openMobileMenu); if (mobileMenuCloseBtn) mobileMenuCloseBtn.addEventListener('click', closeMobileMenu); if (mobileMenuModal) { mobileMenuModal.addEventListener('click', (e) => { if (e.target === mobileMenuModal) closeMobileMenu(); }); } const mobileGrammarBtn = document.getElementById('mobile-grammar-btn'); if (mobileGrammarBtn) { mobileGrammarBtn.addEventListener('click', () => { closeMobileMenu(); openGrammarView(null, true); }); } const mobileCefrBtn = document.getElementById('mobile-cefr-btn'); if (mobileCefrBtn) { mobileCefrBtn.addEventListener('click', () => { closeMobileMenu(); openCefrView(null, 'all', true); }); } if (mobileStatsBtn) { mobileStatsBtn.addEventListener('click', () => { closeMobileMenu(); openStatsDashboard(); }); } if (mobileExportBtn && exportModal) { mobileExportBtn.addEventListener('click', () => { closeMobileMenu(); openModal(exportModal); }); } if (mobilePwdBtn && changePasswordModal) { mobilePwdBtn.addEventListener('click', () => { closeMobileMenu(); openModal(changePasswordModal); }); } if (mobileThemeBtn && themeToggle) { mobileThemeBtn.addEventListener('click', () => { themeToggle.click(); }); } if (mobileLogoutBtn && logoutBtn) { mobileLogoutBtn.addEventListener('click', () => { closeMobileMenu(); logoutBtn.click(); }); } } // ----------------------------------------------------------------------- // 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(); // 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. Mobile Tab Bar & Drawer initMobileNavigation(); // 7. Change Password Modal initChangePasswordModal(); // 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(); });