fix(frontend): export ui object, harmonize API signatures, and clean view switcher
This commit is contained in:
1 parent
718e9797eb
commit
640da1f27c
4 files changed
+138
-147
No files matched your search
+16
-116
@@ -4,7 +4,7 @@
|
|||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
import { api } from './modules/api.js';
|
import { api } from './modules/api.js';
|
||||||
import { initTheme, openModal, closeModal, showToast, playTextSpeech } from './modules/ui.js';
|
import { initTheme, openModal, closeModal, showToast, playTextSpeech, switchView } from './modules/ui.js';
|
||||||
import { initDictionary, loadInitialData } from './modules/dictionary.js';
|
import { initDictionary, loadInitialData } from './modules/dictionary.js';
|
||||||
import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js';
|
import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js';
|
||||||
import { initReader, loadBookshelf, openBook } from './modules/reader.js';
|
import { initReader, loadBookshelf, openBook } from './modules/reader.js';
|
||||||
@@ -81,25 +81,12 @@ export function applyInitialViewPreflight() {
|
|||||||
} catch (e) {}
|
} 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 lessonView = document.getElementById('lesson-view');
|
|
||||||
const lessonRunnerView = document.getElementById('lesson-runner-view');
|
|
||||||
const bookshelfContainer = document.getElementById('bookshelf-container');
|
const bookshelfContainer = document.getElementById('bookshelf-container');
|
||||||
const readerInterface = document.getElementById('reader-interface');
|
const readerInterface = document.getElementById('reader-interface');
|
||||||
const readerBookTitle = document.getElementById('reader-book-title');
|
const readerBookTitle = document.getElementById('reader-book-title');
|
||||||
|
|
||||||
if (hash.startsWith('#reader') || hash === '#bookshelf') {
|
if (hash.startsWith('#reader') || hash === '#bookshelf') {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
switchView('reader-view');
|
||||||
if (srsView) srsView.classList.add('hidden');
|
|
||||||
if (cefrView) cefrView.classList.add('hidden');
|
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (readerView) readerView.classList.remove('hidden');
|
|
||||||
updateActiveNavState('reader');
|
updateActiveNavState('reader');
|
||||||
if (hash.includes('/book/')) {
|
if (hash.includes('/book/')) {
|
||||||
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
|
if (bookshelfContainer) bookshelfContainer.classList.add('hidden');
|
||||||
@@ -107,40 +94,16 @@ export function applyInitialViewPreflight() {
|
|||||||
if (readerBookTitle) readerBookTitle.textContent = 'Загрузка книги...';
|
if (readerBookTitle) readerBookTitle.textContent = 'Загрузка книги...';
|
||||||
}
|
}
|
||||||
} else if (hash.startsWith('#srs')) {
|
} else if (hash.startsWith('#srs')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
switchView('srs-view');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
|
||||||
if (cefrView) cefrView.classList.add('hidden');
|
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (srsView) srsView.classList.remove('hidden');
|
|
||||||
updateActiveNavState('srs');
|
updateActiveNavState('srs');
|
||||||
} else if (hash.startsWith('#cefr')) {
|
} else if (hash.startsWith('#cefr')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
switchView('cefr-view');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
|
||||||
if (srsView) srsView.classList.add('hidden');
|
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (cefrView) cefrView.classList.remove('hidden');
|
|
||||||
updateActiveNavState('cefr');
|
updateActiveNavState('cefr');
|
||||||
} else if (hash.startsWith('#grammar')) {
|
} else if (hash.startsWith('#grammar')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
switchView('grammar-view');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
|
||||||
if (srsView) srsView.classList.add('hidden');
|
|
||||||
if (cefrView) cefrView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (grammarView) grammarView.classList.remove('hidden');
|
|
||||||
updateActiveNavState('grammar');
|
updateActiveNavState('grammar');
|
||||||
} else if (hash.startsWith('#lesson')) {
|
} else if (hash.startsWith('#lesson')) {
|
||||||
if (dictionaryView) dictionaryView.classList.add('hidden');
|
switchView('lesson-view');
|
||||||
if (readerView) readerView.classList.add('hidden');
|
|
||||||
if (srsView) srsView.classList.add('hidden');
|
|
||||||
if (cefrView) cefrView.classList.add('hidden');
|
|
||||||
if (grammarView) grammarView.classList.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.remove('hidden');
|
|
||||||
updateActiveNavState('lesson');
|
updateActiveNavState('lesson');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,17 +116,7 @@ export function openDictionaryView(updateHash = true) {
|
|||||||
window.location.hash = '#dictionary';
|
window.location.hash = '#dictionary';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('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');
|
|
||||||
updateActiveNavState('dict');
|
updateActiveNavState('dict');
|
||||||
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'dictionary-view'); } catch (e) {}
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
@@ -175,17 +128,7 @@ export function openSrsView(tagFilter = null, updateHash = true) {
|
|||||||
window.location.hash = targetHash;
|
window.location.hash = targetHash;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('srs-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');
|
|
||||||
updateActiveNavState('srs');
|
updateActiveNavState('srs');
|
||||||
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'srs-view'); } catch (e) {}
|
||||||
startSrsTrainingSession(false, tagFilter);
|
startSrsTrainingSession(false, tagFilter);
|
||||||
@@ -200,19 +143,9 @@ export function openBookshelfView(updateHash = true) {
|
|||||||
window.location.hash = '#reader';
|
window.location.hash = '#reader';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('reader-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 bookshelfContainer = document.getElementById('bookshelf-container');
|
||||||
const readerInterface = document.getElementById('reader-interface');
|
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 (bookshelfContainer) bookshelfContainer.classList.remove('hidden');
|
||||||
if (readerInterface) readerInterface.classList.add('hidden');
|
if (readerInterface) readerInterface.classList.add('hidden');
|
||||||
updateActiveNavState('reader');
|
updateActiveNavState('reader');
|
||||||
@@ -242,18 +175,7 @@ export function openCefrView(level = null, mode = 'all', updateHash = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('cefr-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');
|
|
||||||
|
|
||||||
updateActiveNavState('cefr');
|
updateActiveNavState('cefr');
|
||||||
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'cefr-view'); } catch (e) {}
|
||||||
|
|
||||||
@@ -272,18 +194,7 @@ export function openGrammarView(unitId = null, updateHash = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('grammar-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');
|
|
||||||
|
|
||||||
updateActiveNavState('grammar');
|
updateActiveNavState('grammar');
|
||||||
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'grammar-view'); } catch (e) {}
|
||||||
|
|
||||||
@@ -301,22 +212,7 @@ export function openLessonView(presetId = null, updateHash = true) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dictionaryView = document.getElementById('dictionary-view');
|
switchView('lesson-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 lessonView = document.getElementById('lesson-view');
|
|
||||||
const lessonRunnerView = document.getElementById('lesson-runner-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.add('hidden');
|
|
||||||
if (lessonRunnerView) lessonRunnerView.classList.add('hidden');
|
|
||||||
if (lessonView) lessonView.classList.remove('hidden');
|
|
||||||
|
|
||||||
updateActiveNavState('lesson');
|
updateActiveNavState('lesson');
|
||||||
try { localStorage.setItem('notes_active_view', 'lesson-view'); } catch (e) {}
|
try { localStorage.setItem('notes_active_view', 'lesson-view'); } catch (e) {}
|
||||||
|
|
||||||
@@ -352,6 +248,10 @@ export async function handleRouting() {
|
|||||||
hash = '#grammar';
|
hash = '#grammar';
|
||||||
window.location.hash = hash;
|
window.location.hash = hash;
|
||||||
return;
|
return;
|
||||||
|
} else if (lastView === 'lesson-view') {
|
||||||
|
hash = '#lesson';
|
||||||
|
window.location.hash = hash;
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
hash = '#dictionary';
|
hash = '#dictionary';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,18 +167,34 @@ export const api = {
|
|||||||
|
|
||||||
// SRS Spaced Repetition
|
// SRS Spaced Repetition
|
||||||
srs: {
|
srs: {
|
||||||
async getQueue(forceAll = false, tagFilter = null) {
|
async getQueue(forceAllOrOptions = false, tagFilter = null) {
|
||||||
let url = '/api/srs/queue?limit=50';
|
let forceAll = false;
|
||||||
|
let tag = tagFilter;
|
||||||
|
let limit = 50;
|
||||||
|
if (typeof forceAllOrOptions === 'object' && forceAllOrOptions !== null) {
|
||||||
|
forceAll = !!forceAllOrOptions.forceAll || !!forceAllOrOptions.force_all;
|
||||||
|
tag = forceAllOrOptions.tagFilter || forceAllOrOptions.tag || tag;
|
||||||
|
if (forceAllOrOptions.limit) limit = forceAllOrOptions.limit;
|
||||||
|
} else {
|
||||||
|
forceAll = !!forceAllOrOptions;
|
||||||
|
}
|
||||||
|
let url = `/api/srs/queue?limit=${limit}`;
|
||||||
if (forceAll) url += '&force_all=true';
|
if (forceAll) url += '&force_all=true';
|
||||||
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
|
if (tag) url += `&tag=${encodeURIComponent(tag)}`;
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
return resp.ok ? await resp.json() : [];
|
return resp.ok ? await resp.json() : { cards: [], count: 0 };
|
||||||
},
|
},
|
||||||
async submitReview(cardId, rating) {
|
async submitReview(cardIdOrPayload, rating) {
|
||||||
|
let cardId = cardIdOrPayload;
|
||||||
|
let r = rating;
|
||||||
|
if (typeof cardIdOrPayload === 'object' && cardIdOrPayload !== null) {
|
||||||
|
cardId = cardIdOrPayload.card_id ?? cardIdOrPayload.id;
|
||||||
|
r = cardIdOrPayload.rating;
|
||||||
|
}
|
||||||
return await fetch('/api/srs/review', {
|
return await fetch('/api/srs/review', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ card_id: cardId, rating })
|
body: JSON.stringify({ card_id: cardId, rating: r })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -194,15 +210,25 @@ export const api = {
|
|||||||
if (resp.ok) return await resp.json();
|
if (resp.ok) return await resp.json();
|
||||||
throw new Error(`Failed to load listening exercise: ${resp.status}`);
|
throw new Error(`Failed to load listening exercise: ${resp.status}`);
|
||||||
},
|
},
|
||||||
async checkAnswer(scenarioId, userAnswer, story = '', idealAnswer = '') {
|
async checkAnswer(scenarioIdOrPayload, userAnswer = '', story = '', idealAnswer = '') {
|
||||||
|
let sId = scenarioIdOrPayload;
|
||||||
|
let uAns = userAnswer;
|
||||||
|
let st = story;
|
||||||
|
let idAns = idealAnswer;
|
||||||
|
if (typeof scenarioIdOrPayload === 'object' && scenarioIdOrPayload !== null) {
|
||||||
|
sId = scenarioIdOrPayload.scenario_id ?? scenarioIdOrPayload.id;
|
||||||
|
uAns = scenarioIdOrPayload.user_answer ?? scenarioIdOrPayload.answer ?? '';
|
||||||
|
st = scenarioIdOrPayload.story ?? '';
|
||||||
|
idAns = scenarioIdOrPayload.ideal_answer ?? scenarioIdOrPayload.idealAnswer ?? '';
|
||||||
|
}
|
||||||
const resp = await fetch('/api/listening/check', {
|
const resp = await fetch('/api/listening/check', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
scenario_id: scenarioId,
|
scenario_id: sId,
|
||||||
user_answer: userAnswer,
|
user_answer: uAns,
|
||||||
story: story,
|
story: st,
|
||||||
ideal_answer: idealAnswer
|
ideal_answer: idAns
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (resp.ok) return await resp.json();
|
if (resp.ok) return await resp.json();
|
||||||
@@ -374,6 +400,9 @@ export const api = {
|
|||||||
if (resp.ok) return await resp.json();
|
if (resp.ok) return await resp.json();
|
||||||
throw new Error(`Failed to complete unit: ${resp.status}`);
|
throw new Error(`Failed to complete unit: ${resp.status}`);
|
||||||
},
|
},
|
||||||
|
async recordCompletion(unitId, score, handwrittenLogged = false) {
|
||||||
|
return this.completeUnit(unitId, score, handwrittenLogged);
|
||||||
|
},
|
||||||
async toggleHandwritten(unitId, handwritten) {
|
async toggleHandwritten(unitId, handwritten) {
|
||||||
const resp = await fetch('/api/grammar/handwritten', {
|
const resp = await fetch('/api/grammar/handwritten', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -385,6 +414,9 @@ export const api = {
|
|||||||
});
|
});
|
||||||
if (resp.ok) return await resp.json();
|
if (resp.ok) return await resp.json();
|
||||||
throw new Error(`Failed to update handwritten status: ${resp.status}`);
|
throw new Error(`Failed to update handwritten status: ${resp.status}`);
|
||||||
|
},
|
||||||
|
async updateHandwritten(unitId, handwritten) {
|
||||||
|
return this.toggleHandwritten(unitId, handwritten);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { ui } from './ui.js';
|
import { ui, escapeHtml, showToast, switchView } from './ui.js';
|
||||||
|
|
||||||
let presets = [];
|
let presets = [];
|
||||||
let currentPreset = null;
|
let currentPreset = null;
|
||||||
@@ -1630,12 +1630,3 @@ async function openHistoryModal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return String(str)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
+78
-10
@@ -3,7 +3,7 @@
|
|||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
export function escapeHtml(str) {
|
export function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (str === null || str === undefined) return '';
|
||||||
return String(str)
|
return String(str)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@@ -13,7 +13,8 @@ export function escapeHtml(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function escapeRegex(string) {
|
export function escapeRegex(string) {
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
if (!string) return '';
|
||||||
|
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugify(text) {
|
export function slugify(text) {
|
||||||
@@ -25,6 +26,7 @@ export function slugify(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function shuffleArray(array) {
|
export function shuffleArray(array) {
|
||||||
|
if (!array || !Array.isArray(array)) return [];
|
||||||
const arr = [...array];
|
const arr = [...array];
|
||||||
for (let i = arr.length - 1; i > 0; i--) {
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
@@ -37,6 +39,7 @@ export function shuffleArray(array) {
|
|||||||
// Toast Notifications
|
// Toast Notifications
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
export function showToast(message, type = 'info') {
|
export function showToast(message, type = 'info') {
|
||||||
|
if (!message) return;
|
||||||
let toastContainer = document.getElementById('notes-toast-container');
|
let toastContainer = document.getElementById('notes-toast-container');
|
||||||
if (!toastContainer) {
|
if (!toastContainer) {
|
||||||
toastContainer = document.createElement('div');
|
toastContainer = document.createElement('div');
|
||||||
@@ -51,7 +54,11 @@ export function showToast(message, type = 'info') {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(toastContainer);
|
if (document.body) {
|
||||||
|
document.body.appendChild(toastContainer);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
@@ -98,14 +105,19 @@ export function showToast(message, type = 'info') {
|
|||||||
export function initTheme() {
|
export function initTheme() {
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
||||||
const savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
let savedTheme = 'dark';
|
||||||
|
try {
|
||||||
|
savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
||||||
|
} catch (e) {}
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
const current = document.documentElement.getAttribute('data-theme');
|
const current = document.documentElement.getAttribute('data-theme');
|
||||||
const next = current === 'dark' ? 'light' : 'dark';
|
const next = current === 'dark' ? 'light' : 'dark';
|
||||||
document.documentElement.setAttribute('data-theme', next);
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
localStorage.setItem('notes_theme', next);
|
try {
|
||||||
|
localStorage.setItem('notes_theme', next);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeToggle) {
|
if (themeToggle) {
|
||||||
@@ -120,16 +132,47 @@ export function initTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Modal Open / Close Helpers
|
// Modal Open / Close Helpers (Accepts HTMLElement or string selector/ID)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
export function openModal(modalElem) {
|
export function openModal(modalElem) {
|
||||||
if (!modalElem) return;
|
if (!modalElem) return;
|
||||||
modalElem.classList.remove('hidden');
|
const el = typeof modalElem === 'string'
|
||||||
|
? (document.getElementById(modalElem) || document.querySelector(modalElem))
|
||||||
|
: modalElem;
|
||||||
|
if (el) el.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeModal(modalElem) {
|
export function closeModal(modalElem) {
|
||||||
if (!modalElem) return;
|
if (!modalElem) return;
|
||||||
modalElem.classList.add('hidden');
|
const el = typeof modalElem === 'string'
|
||||||
|
? (document.getElementById(modalElem) || document.querySelector(modalElem))
|
||||||
|
: modalElem;
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// View Switcher Helper
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
export function switchView(targetViewId) {
|
||||||
|
const allViews = [
|
||||||
|
'dictionary-view',
|
||||||
|
'srs-view',
|
||||||
|
'reader-view',
|
||||||
|
'cefr-view',
|
||||||
|
'grammar-view',
|
||||||
|
'lesson-view',
|
||||||
|
'lesson-runner-view'
|
||||||
|
];
|
||||||
|
allViews.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
if (id === targetViewId) {
|
||||||
|
el.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
el.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -197,11 +240,36 @@ export function playAudio(url, speed = 1.0) {
|
|||||||
export function playTextSpeech(text, lang = 'en', speed = 1.0, voice = null) {
|
export function playTextSpeech(text, lang = 'en', speed = 1.0, voice = null) {
|
||||||
if (!text || !text.trim()) return null;
|
if (!text || !text.trim()) return null;
|
||||||
const isRu = lang && lang.toLowerCase().startsWith('ru');
|
const isRu = lang && lang.toLowerCase().startsWith('ru');
|
||||||
const savedVoice = isRu ? localStorage.getItem('notes_tts_voice_ru') : localStorage.getItem('notes_tts_voice_en');
|
let savedVoice = null;
|
||||||
|
try {
|
||||||
|
savedVoice = isRu
|
||||||
|
? localStorage.getItem('notes_tts_voice_ru')
|
||||||
|
: localStorage.getItem('notes_tts_voice_en');
|
||||||
|
} catch (e) {}
|
||||||
const activeVoice = voice || savedVoice;
|
const activeVoice = voice || savedVoice;
|
||||||
let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang}&speed=${speed}`;
|
let url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang || 'en'}&speed=${speed}`;
|
||||||
if (activeVoice) {
|
if (activeVoice) {
|
||||||
url += `&voice=${encodeURIComponent(activeVoice)}`;
|
url += `&voice=${encodeURIComponent(activeVoice)}`;
|
||||||
}
|
}
|
||||||
return playAudio(url, speed);
|
return playAudio(url, speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Unified UI Object Export (for modules importing `{ ui }` or `ui`)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
export const ui = {
|
||||||
|
escapeHtml,
|
||||||
|
escapeRegex,
|
||||||
|
slugify,
|
||||||
|
shuffleArray,
|
||||||
|
showToast,
|
||||||
|
initTheme,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
switchView,
|
||||||
|
updateHeaderStats,
|
||||||
|
playAudio,
|
||||||
|
playTextSpeech,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ui;
|
||||||
Reference in new issue
Block a user