782 lines
30 KiB
JavaScript
782 lines
30 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();
|
|
});
|