/**
* 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
};
})();