/** * AniWorld - UI Utilities Module * * Toast notifications, loading overlays, and * common UI helper functions. * * Dependencies: constants.js */ var AniWorld = window.AniWorld || {}; AniWorld.UI = (function() { 'use strict'; const DEFAULTS = AniWorld.Constants.DEFAULTS; /** * Show a toast notification * @param {string} message - The message to display * @param {string} type - 'info', 'success', 'warning', or 'error' * @param {number} duration - Duration in milliseconds (optional) */ function showToast(message, type, duration) { type = type || 'info'; duration = duration || DEFAULTS.TOAST_DURATION; const container = document.getElementById('toast-container'); if (!container) { console.warn('Toast container not found'); return; } const toast = document.createElement('div'); toast.className = 'toast ' + type; toast.setAttribute('data-testid', 'toast'); toast.innerHTML = '
' + '' + escapeHtml(message) + '' + '' + '
'; container.appendChild(toast); // Auto-remove after duration setTimeout(function() { if (toast.parentElement) { toast.remove(); } }, duration); } /** * Show loading overlay */ function showLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.classList.remove('hidden'); } } /** * Hide loading overlay */ function hideLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.classList.add('hidden'); } } /** * Escape HTML to prevent XSS * @param {string} text - The text to escape * @returns {string} Escaped HTML */ function escapeHtml(text) { if (text === null || text === undefined) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Format bytes to human readable string * @param {number} bytes - Number of bytes * @param {number} decimals - Decimal places (default 2) * @returns {string} Formatted string like "1.5 MB" */ function formatBytes(bytes, decimals) { decimals = decimals || 2; if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; } /** * Format duration in seconds to human readable string * @param {number} seconds - Duration in seconds * @returns {string} Formatted string like "1h 30m" */ function formatDuration(seconds) { if (!seconds || seconds <= 0) return '---'; if (seconds < 60) { return Math.round(seconds) + 's'; } else if (seconds < 3600) { const minutes = Math.round(seconds / 60); return minutes + 'm'; } else if (seconds < 86400) { const hours = Math.floor(seconds / 3600); const minutes = Math.round((seconds % 3600) / 60); return hours + 'h ' + minutes + 'm'; } else { const days = Math.floor(seconds / 86400); const hours = Math.round((seconds % 86400) / 3600); return days + 'd ' + hours + 'h'; } } /** * Format ETA (alias for formatDuration) * @param {number} seconds - ETA in seconds * @returns {string} Formatted ETA string */ function formatETA(seconds) { return formatDuration(seconds); } /** * Format date to locale string * @param {string|Date} date - Date to format * @returns {string} Formatted date string */ function formatDate(date) { if (!date) return ''; const d = new Date(date); return d.toLocaleString(); } /** * Get display name for anime/series object * Returns name if available, otherwise key or folder * @param {Object} anime - Anime/series object * @returns {string} Display name */ function getDisplayName(anime) { if (!anime) return ''; const name = anime.name || ''; const trimmedName = name.trim(); if (trimmedName) { return trimmedName; } return anime.key || anime.folder || ''; } /** * Calculate duration between two timestamps * @param {string} startTime - Start timestamp * @param {string} endTime - End timestamp * @returns {string} Formatted duration */ function calculateDuration(startTime, endTime) { const start = new Date(startTime); const end = new Date(endTime); const diffMs = end - start; const minutes = Math.floor(diffMs / (1000 * 60)); const seconds = Math.floor((diffMs % (1000 * 60)) / 1000); return minutes + 'm ' + seconds + 's'; } /** * Show a confirmation modal * @param {string} title - Modal title * @param {string} message - Modal message * @returns {Promise} Resolves to true if confirmed, false if cancelled */ function showConfirmModal(title, message) { return new Promise(function(resolve) { const modal = document.getElementById('confirm-modal'); if (!modal) { resolve(window.confirm(message)); return; } document.getElementById('confirm-title').textContent = title; document.getElementById('confirm-message').textContent = message; modal.classList.remove('hidden'); function handleConfirm() { cleanup(); resolve(true); } function handleCancel() { cleanup(); resolve(false); } function cleanup() { document.getElementById('confirm-ok').removeEventListener('click', handleConfirm); document.getElementById('confirm-cancel').removeEventListener('click', handleCancel); modal.classList.add('hidden'); } document.getElementById('confirm-ok').addEventListener('click', handleConfirm); document.getElementById('confirm-cancel').addEventListener('click', handleCancel); }); } /** * Hide the confirmation modal */ function hideConfirmModal() { const modal = document.getElementById('confirm-modal'); if (modal) { modal.classList.add('hidden'); } } // Public API return { showToast: showToast, showLoading: showLoading, hideLoading: hideLoading, escapeHtml: escapeHtml, formatBytes: formatBytes, formatDuration: formatDuration, formatETA: formatETA, formatDate: formatDate, getDisplayName: getDisplayName, calculateDuration: calculateDuration, showConfirmModal: showConfirmModal, hideConfirmModal: hideConfirmModal }; })();