202 lines
6.7 KiB
JavaScript
202 lines
6.7 KiB
JavaScript
// =======================================================================
|
|
// Notes Application - UI & Utilities Module (Native ES Module)
|
|
// =======================================================================
|
|
|
|
export function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
export function escapeRegex(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
export function slugify(text) {
|
|
if (!text) return 'book';
|
|
return text.toString().toLowerCase().trim()
|
|
.replace(/[^\w\s-]/g, '')
|
|
.replace(/[\s_-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
export function shuffleArray(array) {
|
|
const arr = [...array];
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Toast Notifications
|
|
// -----------------------------------------------------------------------
|
|
export function showToast(message, type = 'info') {
|
|
let toastContainer = document.getElementById('notes-toast-container');
|
|
if (!toastContainer) {
|
|
toastContainer = document.createElement('div');
|
|
toastContainer.id = 'notes-toast-container';
|
|
toastContainer.style.cssText = `
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 10000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
pointer-events: none;
|
|
`;
|
|
document.body.appendChild(toastContainer);
|
|
}
|
|
|
|
const toast = document.createElement('div');
|
|
const colors = {
|
|
success: '#10b981',
|
|
error: '#ef4444',
|
|
warning: '#f59e0b',
|
|
info: '#3b82f6'
|
|
};
|
|
const bg = colors[type] || colors.info;
|
|
|
|
toast.style.cssText = `
|
|
background: ${bg};
|
|
color: #ffffff;
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
|
|
opacity: 0;
|
|
transform: translateY(12px);
|
|
transition: all 0.25s ease;
|
|
pointer-events: auto;
|
|
max-width: 340px;
|
|
`;
|
|
toast.textContent = message;
|
|
toastContainer.appendChild(toast);
|
|
|
|
requestAnimationFrame(() => {
|
|
toast.style.opacity = '1';
|
|
toast.style.transform = 'translateY(0)';
|
|
});
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
toast.style.transform = 'translateY(12px)';
|
|
setTimeout(() => toast.remove(), 250);
|
|
}, 3500);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Theme Management
|
|
// -----------------------------------------------------------------------
|
|
export function initTheme() {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const mobileThemeBtn = document.getElementById('mobile-theme-btn');
|
|
const savedTheme = localStorage.getItem('notes_theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
function toggle() {
|
|
const current = document.documentElement.getAttribute('data-theme');
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', next);
|
|
localStorage.setItem('notes_theme', next);
|
|
}
|
|
|
|
if (themeToggle) {
|
|
themeToggle.addEventListener('click', toggle);
|
|
}
|
|
if (mobileThemeBtn) {
|
|
mobileThemeBtn.addEventListener('click', () => {
|
|
toggle();
|
|
showToast(`Тема переключена`, 'info');
|
|
});
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Modal Open / Close Helpers
|
|
// -----------------------------------------------------------------------
|
|
export function openModal(modalElem) {
|
|
if (!modalElem) return;
|
|
modalElem.classList.remove('hidden');
|
|
}
|
|
|
|
export function closeModal(modalElem) {
|
|
if (!modalElem) return;
|
|
modalElem.classList.add('hidden');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Header Stat Counters
|
|
// -----------------------------------------------------------------------
|
|
export function updateHeaderStats(total = 0, today = 0, srsCount = 0) {
|
|
const statTotal = document.getElementById('stat-total');
|
|
const statToday = document.getElementById('stat-today');
|
|
const srsBadge = document.getElementById('srs-badge');
|
|
const tabSrsBadge = document.getElementById('tab-srs-badge');
|
|
const drawerSrsBadge = document.getElementById('drawer-srs-badge');
|
|
const drawerStatTotal = document.getElementById('drawer-stat-total');
|
|
const drawerStatToday = document.getElementById('drawer-stat-today');
|
|
const drawerStatDue = document.getElementById('drawer-stat-due');
|
|
|
|
const formattedToday = today > 0 ? `+${today}` : today;
|
|
|
|
if (statTotal) statTotal.textContent = total;
|
|
if (statToday) statToday.textContent = formattedToday;
|
|
if (drawerStatTotal) drawerStatTotal.textContent = total;
|
|
if (drawerStatToday) drawerStatToday.textContent = formattedToday;
|
|
if (drawerStatDue) drawerStatDue.textContent = srsCount;
|
|
|
|
if (srsBadge) {
|
|
srsBadge.textContent = srsCount;
|
|
srsBadge.style.display = srsCount > 0 ? 'inline-block' : 'none';
|
|
}
|
|
if (tabSrsBadge) {
|
|
tabSrsBadge.textContent = srsCount;
|
|
tabSrsBadge.classList.toggle('hidden', srsCount === 0);
|
|
}
|
|
if (drawerSrsBadge) {
|
|
drawerSrsBadge.textContent = srsCount;
|
|
drawerSrsBadge.classList.toggle('hidden', srsCount === 0);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Audio Player Helper (Safe stub / audio stream / Piper TTS)
|
|
// -----------------------------------------------------------------------
|
|
let activeAudio = null;
|
|
|
|
export function playAudio(url, speed = 1.0) {
|
|
if (!url) return null;
|
|
try {
|
|
if (activeAudio) {
|
|
try {
|
|
activeAudio.pause();
|
|
activeAudio.currentTime = 0;
|
|
} catch (e) {}
|
|
}
|
|
const audio = new Audio(url);
|
|
if (speed && speed !== 1.0) {
|
|
audio.playbackRate = speed;
|
|
}
|
|
activeAudio = audio;
|
|
audio.play().catch(e => console.warn('Audio playback not supported or blocked:', e));
|
|
return audio;
|
|
} catch (e) {
|
|
console.warn('Audio play error:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function playTextSpeech(text, lang = 'en', speed = 1.0) {
|
|
if (!text || !text.trim()) return null;
|
|
const url = `/api/tts?text=${encodeURIComponent(text.trim())}&lang=${lang}&speed=${speed}`;
|
|
return playAudio(url, speed);
|
|
}
|