651 lines
24 KiB
JavaScript
651 lines
24 KiB
JavaScript
// Notes Application Frontend Logic
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// State
|
|
let currentUser = null;
|
|
let currentLookupData = null;
|
|
let allCards = [];
|
|
let selectedTag = '';
|
|
let searchQuery = '';
|
|
|
|
// Elements - Auth
|
|
const authScreen = document.getElementById('auth-screen');
|
|
const appContainer = document.getElementById('app-container');
|
|
const loginForm = document.getElementById('login-form');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const authError = document.getElementById('auth-error');
|
|
const loginBtn = document.getElementById('login-btn');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const userDisplayName = document.getElementById('user-display-name');
|
|
|
|
// Elements - Navbar & Stats
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const statTotal = document.getElementById('stat-total');
|
|
const statToday = document.getElementById('stat-today');
|
|
|
|
// Elements - Search & Result
|
|
const termInput = document.getElementById('term-input');
|
|
const clearSearchBtn = document.getElementById('clear-search-btn');
|
|
const translateBtn = document.getElementById('translate-btn');
|
|
const lookupSpinner = document.getElementById('lookup-spinner');
|
|
const resultCard = document.getElementById('result-card');
|
|
const resTerm = document.getElementById('res-term');
|
|
const resPhonetic = document.getElementById('res-phonetic');
|
|
const resPos = document.getElementById('res-pos');
|
|
const playAudioBtn = document.getElementById('play-audio-btn');
|
|
const resDefinitionBox = document.getElementById('res-definition-box');
|
|
const resDefinition = document.getElementById('res-definition');
|
|
const editTranslation = document.getElementById('edit-translation');
|
|
const translationSuggestions = document.getElementById('translation-suggestions');
|
|
const editContext = document.getElementById('edit-context');
|
|
const editTags = document.getElementById('edit-tags');
|
|
const quickTagsBar = document.getElementById('quick-tags-bar');
|
|
const editNotes = document.getElementById('edit-notes');
|
|
const saveCardBtn = document.getElementById('save-card-btn');
|
|
|
|
// Elements - Vocabulary List
|
|
const filterInput = document.getElementById('filter-input');
|
|
const tagsFilterBar = document.getElementById('tags-filter-bar');
|
|
const cardsList = document.getElementById('cards-list');
|
|
const emptyState = document.getElementById('empty-state');
|
|
const cardsCountLabel = document.getElementById('cards-count-label');
|
|
|
|
// Elements - Modal
|
|
const editModal = document.getElementById('edit-modal');
|
|
const closeModalBtn = document.getElementById('close-modal-btn');
|
|
const modalCancelBtn = document.getElementById('modal-cancel-btn');
|
|
const modalSaveBtn = document.getElementById('modal-save-btn');
|
|
const modalCardId = document.getElementById('modal-card-id');
|
|
const modalTerm = document.getElementById('modal-term');
|
|
const modalTranslation = document.getElementById('modal-translation');
|
|
const modalContext = document.getElementById('modal-context');
|
|
const modalTags = document.getElementById('modal-tags');
|
|
const modalNotes = document.getElementById('modal-notes');
|
|
|
|
// Audio
|
|
const globalAudioPlayer = document.getElementById('global-audio-player');
|
|
|
|
// Theme Management
|
|
function initTheme() {
|
|
const savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙';
|
|
}
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('notes_theme', newTheme);
|
|
themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
|
|
});
|
|
|
|
// Pronunciation Helper (Audio file or Web Speech API fallback)
|
|
function playAudio(text, audioUrl) {
|
|
if (audioUrl) {
|
|
globalAudioPlayer.src = audioUrl;
|
|
globalAudioPlayer.play().catch(() => {
|
|
fallbackSpeechSynthesis(text);
|
|
});
|
|
} else {
|
|
fallbackSpeechSynthesis(text);
|
|
}
|
|
}
|
|
|
|
function fallbackSpeechSynthesis(text) {
|
|
if ('speechSynthesis' in window) {
|
|
window.speechSynthesis.cancel();
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
utterance.lang = 'en-US';
|
|
utterance.rate = 0.9;
|
|
window.speechSynthesis.speak(utterance);
|
|
}
|
|
}
|
|
|
|
// Auth Handlers
|
|
async function checkAuth() {
|
|
try {
|
|
const resp = await fetch('/api/me');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
currentUser = data.user;
|
|
showApp();
|
|
} else {
|
|
showAuth();
|
|
}
|
|
} catch (e) {
|
|
showAuth();
|
|
}
|
|
}
|
|
|
|
function showAuth() {
|
|
authScreen.classList.remove('hidden');
|
|
appContainer.classList.add('hidden');
|
|
usernameInput.focus();
|
|
}
|
|
|
|
function showApp() {
|
|
authScreen.classList.add('hidden');
|
|
appContainer.classList.remove('hidden');
|
|
userDisplayName.textContent = currentUser ? currentUser.username : 'user';
|
|
termInput.focus();
|
|
loadStats();
|
|
loadCards();
|
|
loadTags();
|
|
}
|
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
authError.classList.add('hidden');
|
|
loginBtn.disabled = true;
|
|
loginBtn.textContent = 'Вход...';
|
|
|
|
try {
|
|
const resp = await fetch('/api/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: usernameInput.value.trim(),
|
|
password: passwordInput.value
|
|
})
|
|
});
|
|
|
|
const data = await resp.json();
|
|
if (resp.ok) {
|
|
currentUser = data.user;
|
|
passwordInput.value = '';
|
|
showApp();
|
|
} else {
|
|
authError.textContent = data.detail || 'Ошибка авторизации';
|
|
authError.classList.remove('hidden');
|
|
}
|
|
} catch (err) {
|
|
authError.textContent = 'Ошибка сети. Проверьте подключение к серверу.';
|
|
authError.classList.remove('hidden');
|
|
} finally {
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = 'Войти в систему';
|
|
}
|
|
});
|
|
|
|
logoutBtn.addEventListener('click', async () => {
|
|
try {
|
|
await fetch('/api/logout', { method: 'POST' });
|
|
} catch (e) {}
|
|
currentUser = null;
|
|
showAuth();
|
|
});
|
|
|
|
// Stats
|
|
async function loadStats() {
|
|
try {
|
|
const resp = await fetch('/api/stats');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
statTotal.textContent = data.total_cards;
|
|
statToday.textContent = `+${data.today_added}`;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Search & Translate
|
|
async function performLookup() {
|
|
const query = termInput.value.trim();
|
|
if (!query) return;
|
|
|
|
lookupSpinner.classList.remove('hidden');
|
|
resultCard.classList.add('hidden');
|
|
translateBtn.disabled = true;
|
|
|
|
try {
|
|
const resp = await fetch(`/api/lookup?q=${encodeURIComponent(query)}`);
|
|
if (resp.status === 401) {
|
|
showAuth();
|
|
return;
|
|
}
|
|
if (!resp.ok) throw new Error('Ошибка словаря');
|
|
|
|
const data = await resp.json();
|
|
currentLookupData = data;
|
|
renderResultCard(data);
|
|
} catch (err) {
|
|
alert('Не удалось получить перевод. Попробуйте снова или введите перевод вручную.');
|
|
} finally {
|
|
lookupSpinner.classList.add('hidden');
|
|
translateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function renderResultCard(data) {
|
|
resTerm.textContent = data.query;
|
|
|
|
// Phonetic
|
|
if (data.phonetic) {
|
|
resPhonetic.textContent = data.phonetic;
|
|
resPhonetic.classList.remove('hidden');
|
|
} else {
|
|
resPhonetic.classList.add('hidden');
|
|
}
|
|
|
|
// Part of Speech
|
|
if (data.part_of_speech) {
|
|
resPos.textContent = data.part_of_speech;
|
|
resPos.classList.remove('hidden');
|
|
} else {
|
|
resPos.classList.add('hidden');
|
|
}
|
|
|
|
// Definitions
|
|
if (data.definitions && data.definitions.length > 0) {
|
|
resDefinition.textContent = data.definitions[0];
|
|
resDefinitionBox.classList.remove('hidden');
|
|
} else {
|
|
resDefinitionBox.classList.add('hidden');
|
|
}
|
|
|
|
// Translation field
|
|
editTranslation.value = data.translation || '';
|
|
|
|
// Translation suggestions chips
|
|
translationSuggestions.innerHTML = '';
|
|
if (data.translations_list && data.translations_list.length > 1) {
|
|
data.translations_list.forEach(t => {
|
|
const chip = document.createElement('span');
|
|
chip.className = 'suggestion-chip';
|
|
chip.textContent = t;
|
|
chip.addEventListener('click', () => {
|
|
editTranslation.value = t;
|
|
});
|
|
translationSuggestions.appendChild(chip);
|
|
});
|
|
}
|
|
|
|
// Context: if user entered a sentence, put it in context field
|
|
if (data.is_sentence) {
|
|
editContext.value = data.query;
|
|
} else {
|
|
editContext.value = '';
|
|
}
|
|
|
|
// Auto-assign default tags
|
|
let defaultTags = ['general'];
|
|
if (data.is_sentence) {
|
|
defaultTags = ['phrase'];
|
|
} else if (data.query.includes(' ') && data.part_of_speech && data.part_of_speech.toLowerCase().includes('verb')) {
|
|
defaultTags = ['phrasal-verb'];
|
|
}
|
|
editTags.value = defaultTags.join(', ');
|
|
editNotes.value = '';
|
|
|
|
resultCard.classList.remove('hidden');
|
|
editTranslation.focus();
|
|
}
|
|
|
|
// Audio Play for Result Card
|
|
playAudioBtn.addEventListener('click', () => {
|
|
if (!currentLookupData) return;
|
|
playAudio(currentLookupData.query, currentLookupData.audio_url);
|
|
});
|
|
|
|
// Quick Tags Selector
|
|
quickTagsBar.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('tag-chip')) {
|
|
const tag = e.target.dataset.tag;
|
|
const currentTags = editTags.value.split(',').map(t => t.trim()).filter(Boolean);
|
|
if (!currentTags.includes(tag)) {
|
|
currentTags.push(tag);
|
|
editTags.value = currentTags.join(', ');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Save Card
|
|
async function saveCurrentCard() {
|
|
if (!currentLookupData) return;
|
|
const term = currentLookupData.query;
|
|
const translation = editTranslation.value.trim();
|
|
|
|
if (!translation) {
|
|
alert('Пожалуйста, укажите перевод слова.');
|
|
editTranslation.focus();
|
|
return;
|
|
}
|
|
|
|
saveCardBtn.disabled = true;
|
|
saveCardBtn.textContent = 'Сохранение...';
|
|
|
|
try {
|
|
const payload = {
|
|
term: term,
|
|
translation: translation,
|
|
context_sentence: editContext.value.trim(),
|
|
phonetic: currentLookupData.phonetic || '',
|
|
audio_url: currentLookupData.audio_url || '',
|
|
part_of_speech: currentLookupData.part_of_speech || '',
|
|
definition_en: currentLookupData.definitions && currentLookupData.definitions.length ? currentLookupData.definitions[0] : '',
|
|
tags: editTags.value.trim() || 'general',
|
|
notes: editNotes.value.trim()
|
|
};
|
|
|
|
const resp = await fetch('/api/cards', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (resp.ok) {
|
|
// Success feedback
|
|
saveCardBtn.textContent = '✅ Сохранено!';
|
|
setTimeout(() => {
|
|
resultCard.classList.add('hidden');
|
|
termInput.value = '';
|
|
clearSearchBtn.classList.add('hidden');
|
|
termInput.focus();
|
|
saveCardBtn.textContent = '💾 Сохранить в словарь (Ctrl + Enter)';
|
|
saveCardBtn.disabled = false;
|
|
}, 600);
|
|
|
|
loadCards();
|
|
loadStats();
|
|
loadTags();
|
|
} else {
|
|
throw new Error('Ошибка сохранения');
|
|
}
|
|
} catch (err) {
|
|
alert('Не удалось сохранить карточку. Попробуйте еще раз.');
|
|
saveCardBtn.disabled = false;
|
|
saveCardBtn.textContent = '💾 Сохранить в словарь (Ctrl + Enter)';
|
|
}
|
|
}
|
|
|
|
saveCardBtn.addEventListener('click', saveCurrentCard);
|
|
|
|
// Term Input controls
|
|
termInput.addEventListener('input', () => {
|
|
if (termInput.value.trim()) {
|
|
clearSearchBtn.classList.remove('hidden');
|
|
} else {
|
|
clearSearchBtn.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
clearSearchBtn.addEventListener('click', () => {
|
|
termInput.value = '';
|
|
clearSearchBtn.classList.add('hidden');
|
|
resultCard.classList.add('hidden');
|
|
termInput.focus();
|
|
});
|
|
|
|
translateBtn.addEventListener('click', performLookup);
|
|
|
|
// Global Hotkeys
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl+Enter or Cmd+Enter to Save Card
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
if (!resultCard.classList.contains('hidden')) {
|
|
saveCurrentCard();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Enter in search input
|
|
if (e.key === 'Enter' && document.activeElement === termInput) {
|
|
e.preventDefault();
|
|
performLookup();
|
|
return;
|
|
}
|
|
|
|
// Esc to clear search or close modal
|
|
if (e.key === 'Escape') {
|
|
if (!editModal.classList.contains('hidden')) {
|
|
editModal.classList.add('hidden');
|
|
} else if (document.activeElement === termInput) {
|
|
termInput.value = '';
|
|
clearSearchBtn.classList.add('hidden');
|
|
resultCard.classList.add('hidden');
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
// Vocabulary Cards & Tags
|
|
async function loadCards() {
|
|
try {
|
|
let url = `/api/cards?limit=150`;
|
|
if (searchQuery) url += `&search=${encodeURIComponent(searchQuery)}`;
|
|
if (selectedTag) url += `&tag=${encodeURIComponent(selectedTag)}`;
|
|
|
|
const resp = await fetch(url);
|
|
if (resp.status === 401) {
|
|
showAuth();
|
|
return;
|
|
}
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
allCards = data.cards;
|
|
cardsCountLabel.textContent = `(${data.total} записей)`;
|
|
renderCards(allCards);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderCards(cards) {
|
|
cardsList.innerHTML = '';
|
|
if (!cards || cards.length === 0) {
|
|
emptyState.classList.remove('hidden');
|
|
cardsList.appendChild(emptyState);
|
|
return;
|
|
}
|
|
emptyState.classList.add('hidden');
|
|
|
|
cards.forEach(card => {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.className = 'vocab-card';
|
|
|
|
// Format date
|
|
const cardDate = new Date(card.created_at);
|
|
const dateStr = cardDate.toLocaleDateString('ru-RU', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
|
|
// Format tags
|
|
const tagsList = (card.tags || '').split(',').map(t => t.trim()).filter(Boolean);
|
|
const tagsHtml = tagsList.map(t => `<span class="card-tag">#${escapeHtml(t)}</span>`).join('');
|
|
|
|
// Highlight context
|
|
let contextHtml = '';
|
|
if (card.context_sentence) {
|
|
let formattedContext = escapeHtml(card.context_sentence);
|
|
// Highlight term occurrence in context
|
|
const regex = new RegExp(`(${escapeRegExp(card.term)})`, 'gi');
|
|
formattedContext = formattedContext.replace(regex, '<mark>$1</mark>');
|
|
contextHtml = `<div class="card-context">“${formattedContext}”</div>`;
|
|
}
|
|
|
|
// Notes
|
|
let notesHtml = '';
|
|
if (card.notes) {
|
|
notesHtml = `<span class="card-notes" title="Заметка">💡 ${escapeHtml(card.notes)}</span>`;
|
|
}
|
|
|
|
cardEl.innerHTML = `
|
|
<div class="card-top">
|
|
<div class="card-term-group">
|
|
<span class="card-term">${escapeHtml(card.term)}</span>
|
|
${card.phonetic ? `<span class="card-phonetic">${escapeHtml(card.phonetic)}</span>` : ''}
|
|
${card.part_of_speech ? `<span class="card-pos">${escapeHtml(card.part_of_speech)}</span>` : ''}
|
|
<button class="icon-btn play-card-audio-btn" title="Озвучить" data-term="${escapeHtml(card.term)}" data-audio="${escapeHtml(card.audio_url || '')}">🔊</button>
|
|
</div>
|
|
<div class="card-actions">
|
|
<button class="icon-btn edit-card-btn" title="Редактировать" data-id="${card.id}">✏️</button>
|
|
<button class="icon-btn delete-card-btn" title="Удалить" data-id="${card.id}">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-translation">${escapeHtml(card.translation)}</div>
|
|
${contextHtml}
|
|
|
|
<div class="card-bottom">
|
|
<div class="card-tags">${tagsHtml}</div>
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
${notesHtml}
|
|
<span class="card-date">${dateStr}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Card Listeners
|
|
cardEl.querySelector('.play-card-audio-btn').addEventListener('click', (e) => {
|
|
const btn = e.currentTarget;
|
|
playAudio(btn.dataset.term, btn.dataset.audio);
|
|
});
|
|
|
|
cardEl.querySelector('.edit-card-btn').addEventListener('click', () => {
|
|
openEditModal(card);
|
|
});
|
|
|
|
cardEl.querySelector('.delete-card-btn').addEventListener('click', async () => {
|
|
if (confirm(`Удалить слово "${card.term}" из словаря?`)) {
|
|
await deleteCard(card.id);
|
|
}
|
|
});
|
|
|
|
cardsList.appendChild(cardEl);
|
|
});
|
|
}
|
|
|
|
async function deleteCard(cardId) {
|
|
try {
|
|
const resp = await fetch(`/api/cards/${cardId}`, { method: 'DELETE' });
|
|
if (resp.ok) {
|
|
loadCards();
|
|
loadStats();
|
|
loadTags();
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка удаления карточки');
|
|
}
|
|
}
|
|
|
|
// Modal Edit
|
|
function openEditModal(card) {
|
|
modalCardId.value = card.id;
|
|
modalTerm.value = card.term;
|
|
modalTranslation.value = card.translation;
|
|
modalContext.value = card.context_sentence || '';
|
|
modalTags.value = card.tags || '';
|
|
modalNotes.value = card.notes || '';
|
|
editModal.classList.remove('hidden');
|
|
modalTranslation.focus();
|
|
}
|
|
|
|
closeModalBtn.addEventListener('click', () => editModal.classList.add('hidden'));
|
|
modalCancelBtn.addEventListener('click', () => editModal.classList.add('hidden'));
|
|
|
|
modalSaveBtn.addEventListener('click', async () => {
|
|
const id = modalCardId.value;
|
|
try {
|
|
const payload = {
|
|
term: modalTerm.value.trim(),
|
|
translation: modalTranslation.value.trim(),
|
|
context_sentence: modalContext.value.trim(),
|
|
tags: modalTags.value.trim(),
|
|
notes: modalNotes.value.trim()
|
|
};
|
|
const resp = await fetch(`/api/cards/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (resp.ok) {
|
|
editModal.classList.add('hidden');
|
|
loadCards();
|
|
loadTags();
|
|
} else {
|
|
alert('Не удалось обновить карточку.');
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка при сохранении.');
|
|
}
|
|
});
|
|
|
|
// Tag Filter Bar
|
|
async function loadTags() {
|
|
try {
|
|
const resp = await fetch('/api/tags');
|
|
if (resp.ok) {
|
|
const tags = await resp.json();
|
|
renderTagsFilter(tags);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderTagsFilter(tags) {
|
|
tagsFilterBar.innerHTML = '';
|
|
|
|
// "All tags" button
|
|
const allBtn = document.createElement('button');
|
|
allBtn.className = `filter-chip ${selectedTag === '' ? 'active' : ''}`;
|
|
allBtn.textContent = 'Все теги';
|
|
allBtn.addEventListener('click', () => {
|
|
selectedTag = '';
|
|
document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active'));
|
|
allBtn.classList.add('active');
|
|
loadCards();
|
|
});
|
|
tagsFilterBar.appendChild(allBtn);
|
|
|
|
tags.forEach(({ tag, count }) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = `filter-chip ${selectedTag === tag ? 'active' : ''}`;
|
|
btn.textContent = `#${tag} (${count})`;
|
|
btn.addEventListener('click', () => {
|
|
if (selectedTag === tag) {
|
|
selectedTag = '';
|
|
btn.classList.remove('active');
|
|
allBtn.classList.add('active');
|
|
} else {
|
|
selectedTag = tag;
|
|
document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
}
|
|
loadCards();
|
|
});
|
|
tagsFilterBar.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
// Mini search filter
|
|
let debounceTimer = null;
|
|
filterInput.addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => {
|
|
searchQuery = filterInput.value.trim();
|
|
loadCards();
|
|
}, 250);
|
|
});
|
|
|
|
// Helper functions
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function escapeRegExp(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
// Initialize
|
|
initTheme();
|
|
checkAuth();
|
|
});
|