refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
245
src/server/web/static/js/shared/ui-utils.js
Normal file
245
src/server/web/static/js/shared/ui-utils.js
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.innerHTML =
|
||||
'<div style="display: flex; justify-content: space-between; align-items: center;">' +
|
||||
'<span>' + escapeHtml(message) + '</span>' +
|
||||
'<button onclick="this.parentElement.parentElement.remove()" ' +
|
||||
'style="background: none; border: none; color: var(--color-text-secondary); ' +
|
||||
'cursor: pointer; padding: 0; margin-left: 1rem;">' +
|
||||
'<i class="fas fa-times"></i>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
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<boolean>} 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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user