Files
notes-qpid-pro/app/static/js/app.js
T

782 lines
31 KiB
JavaScript

// =======================================================================
// 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 } 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';
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);
// Escape Key Support
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && drawer && drawer.classList.contains('active')) {
closeDrawer();
}
});
// 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();
// 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();
});