refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
120
src/server/web/static/js/shared/api-client.js
Normal file
120
src/server/web/static/js/shared/api-client.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* AniWorld - API Client Module
|
||||
*
|
||||
* HTTP request wrapper with automatic authentication
|
||||
* and error handling.
|
||||
*
|
||||
* Dependencies: constants.js, auth.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.ApiClient = (function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Make an authenticated HTTP request
|
||||
* Automatically includes Authorization header and handles 401 responses
|
||||
*
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} options - Fetch options (method, headers, body, etc.)
|
||||
* @returns {Promise<Response|null>} The fetch response or null if auth failed
|
||||
*/
|
||||
async function request(url, options) {
|
||||
options = options || {};
|
||||
|
||||
// Get JWT token from localStorage
|
||||
const token = AniWorld.Auth.getToken();
|
||||
|
||||
// Check if token exists
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build request options with auth header
|
||||
const requestOptions = {
|
||||
credentials: 'same-origin',
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
...options.headers
|
||||
}
|
||||
};
|
||||
|
||||
// Add Content-Type for JSON body if not already set
|
||||
if (options.body && typeof options.body === 'string' && !requestOptions.headers['Content-Type']) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token is invalid or expired, clear it and redirect to login
|
||||
AniWorld.Auth.removeToken();
|
||||
window.location.href = '/login';
|
||||
return null;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} headers - Additional headers
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async function get(url, headers) {
|
||||
return request(url, { method: 'GET', headers: headers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a POST request with JSON body
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} data - The data to send
|
||||
* @param {Object} headers - Additional headers
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async function post(url, data, headers) {
|
||||
return request(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} headers - Additional headers
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async function del(url, headers) {
|
||||
return request(url, { method: 'DELETE', headers: headers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a PUT request with JSON body
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} data - The data to send
|
||||
* @param {Object} headers - Additional headers
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async function put(url, data, headers) {
|
||||
return request(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
request: request,
|
||||
get: get,
|
||||
post: post,
|
||||
delete: del,
|
||||
put: put
|
||||
};
|
||||
})();
|
||||
173
src/server/web/static/js/shared/auth.js
Normal file
173
src/server/web/static/js/shared/auth.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* AniWorld - Authentication Module
|
||||
*
|
||||
* Handles user authentication, token management,
|
||||
* and session validation.
|
||||
*
|
||||
* Dependencies: constants.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.Auth = (function() {
|
||||
'use strict';
|
||||
|
||||
const STORAGE = AniWorld.Constants.STORAGE_KEYS;
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Get the stored access token
|
||||
* @returns {string|null} The access token or null if not found
|
||||
*/
|
||||
function getToken() {
|
||||
return localStorage.getItem(STORAGE.ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the access token
|
||||
* @param {string} token - The access token to store
|
||||
*/
|
||||
function setToken(token) {
|
||||
localStorage.setItem(STORAGE.ACCESS_TOKEN, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the stored access token
|
||||
*/
|
||||
function removeToken() {
|
||||
localStorage.removeItem(STORAGE.ACCESS_TOKEN);
|
||||
localStorage.removeItem(STORAGE.TOKEN_EXPIRES_AT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization headers for API requests
|
||||
* @returns {Object} Headers object with Authorization if token exists
|
||||
*/
|
||||
function getAuthHeaders() {
|
||||
const token = getToken();
|
||||
return token ? { 'Authorization': 'Bearer ' + token } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* Redirects to login page if not authenticated
|
||||
* @returns {Promise<boolean>} True if authenticated, false otherwise
|
||||
*/
|
||||
async function checkAuth() {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Don't check authentication if already on login or setup pages
|
||||
if (currentPath === '/login' || currentPath === '/setup') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = getToken();
|
||||
console.log('checkAuthentication: token exists =', !!token);
|
||||
|
||||
if (!token) {
|
||||
console.log('checkAuthentication: No token found, redirecting to /login');
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + token
|
||||
};
|
||||
|
||||
const response = await fetch(API.AUTH_STATUS, { headers });
|
||||
console.log('checkAuthentication: response status =', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('checkAuthentication: Response not OK, status =', response.status);
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('checkAuthentication: data =', data);
|
||||
|
||||
if (!data.configured) {
|
||||
console.log('checkAuthentication: Not configured, redirecting to /setup');
|
||||
window.location.href = '/setup';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.authenticated) {
|
||||
console.log('checkAuthentication: Not authenticated, redirecting to /login');
|
||||
removeToken();
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('checkAuthentication: Authenticated successfully');
|
||||
|
||||
// Show logout button if it exists
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.style.display = 'block';
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Authentication check failed:', error);
|
||||
removeToken();
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out the current user
|
||||
* @param {Function} showToast - Optional function to show toast messages
|
||||
*/
|
||||
async function logout(showToast) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.request(API.AUTH_LOGOUT, { method: 'POST' });
|
||||
|
||||
removeToken();
|
||||
|
||||
if (response && response.ok) {
|
||||
const data = await response.json();
|
||||
if (showToast) {
|
||||
showToast(data.status === 'ok' ? 'Logged out successfully' : 'Logged out', 'success');
|
||||
}
|
||||
} else {
|
||||
if (showToast) {
|
||||
showToast('Logged out', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.href = '/login';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
removeToken();
|
||||
if (showToast) {
|
||||
showToast('Logged out', 'success');
|
||||
}
|
||||
setTimeout(function() {
|
||||
window.location.href = '/login';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a valid token stored
|
||||
* @returns {boolean} True if token exists
|
||||
*/
|
||||
function hasToken() {
|
||||
return !!getToken();
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
getToken: getToken,
|
||||
setToken: setToken,
|
||||
removeToken: removeToken,
|
||||
getAuthHeaders: getAuthHeaders,
|
||||
checkAuth: checkAuth,
|
||||
logout: logout,
|
||||
hasToken: hasToken
|
||||
};
|
||||
})();
|
||||
147
src/server/web/static/js/shared/constants.js
Normal file
147
src/server/web/static/js/shared/constants.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* AniWorld - Constants Module
|
||||
*
|
||||
* Shared constants, API endpoints, and configuration values
|
||||
* used across all JavaScript modules.
|
||||
*
|
||||
* Dependencies: None (must be loaded first)
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.Constants = (function() {
|
||||
'use strict';
|
||||
|
||||
// API Endpoints
|
||||
const API = {
|
||||
// Auth endpoints
|
||||
AUTH_STATUS: '/api/auth/status',
|
||||
AUTH_LOGIN: '/api/auth/login',
|
||||
AUTH_LOGOUT: '/api/auth/logout',
|
||||
|
||||
// Anime endpoints
|
||||
ANIME_LIST: '/api/anime',
|
||||
ANIME_SEARCH: '/api/anime/search',
|
||||
ANIME_ADD: '/api/anime/add',
|
||||
ANIME_RESCAN: '/api/anime/rescan',
|
||||
ANIME_STATUS: '/api/anime/status',
|
||||
ANIME_SCAN_STATUS: '/api/anime/scan/status',
|
||||
|
||||
// Queue endpoints
|
||||
QUEUE_STATUS: '/api/queue/status',
|
||||
QUEUE_ADD: '/api/queue/add',
|
||||
QUEUE_START: '/api/queue/start',
|
||||
QUEUE_STOP: '/api/queue/stop',
|
||||
QUEUE_RETRY: '/api/queue/retry',
|
||||
QUEUE_REMOVE: '/api/queue', // + /{id}
|
||||
QUEUE_COMPLETED: '/api/queue/completed',
|
||||
QUEUE_FAILED: '/api/queue/failed',
|
||||
QUEUE_PENDING: '/api/queue/pending',
|
||||
|
||||
// Config endpoints
|
||||
CONFIG_DIRECTORY: '/api/config/directory',
|
||||
CONFIG_SECTION: '/api/config/section', // + /{section}
|
||||
CONFIG_BACKUP: '/api/config/backup',
|
||||
CONFIG_BACKUPS: '/api/config/backups',
|
||||
CONFIG_VALIDATE: '/api/config/validate',
|
||||
CONFIG_RESET: '/api/config/reset',
|
||||
|
||||
// Scheduler endpoints
|
||||
SCHEDULER_CONFIG: '/api/scheduler/config',
|
||||
SCHEDULER_TRIGGER: '/api/scheduler/trigger-rescan',
|
||||
|
||||
// Logging endpoints
|
||||
LOGGING_CONFIG: '/api/logging/config',
|
||||
LOGGING_FILES: '/api/logging/files',
|
||||
LOGGING_CLEANUP: '/api/logging/cleanup',
|
||||
LOGGING_TEST: '/api/logging/test',
|
||||
|
||||
// Diagnostics
|
||||
DIAGNOSTICS_NETWORK: '/api/diagnostics/network'
|
||||
};
|
||||
|
||||
// Local Storage Keys
|
||||
const STORAGE_KEYS = {
|
||||
ACCESS_TOKEN: 'access_token',
|
||||
TOKEN_EXPIRES_AT: 'token_expires_at',
|
||||
THEME: 'theme'
|
||||
};
|
||||
|
||||
// Default Values
|
||||
const DEFAULTS = {
|
||||
THEME: 'light',
|
||||
TOAST_DURATION: 5000,
|
||||
SCAN_AUTO_DISMISS: 3000,
|
||||
REFRESH_INTERVAL: 2000
|
||||
};
|
||||
|
||||
// WebSocket Rooms
|
||||
const WS_ROOMS = {
|
||||
DOWNLOADS: 'downloads',
|
||||
QUEUE: 'queue',
|
||||
SCAN: 'scan',
|
||||
SYSTEM: 'system',
|
||||
ERRORS: 'errors'
|
||||
};
|
||||
|
||||
// WebSocket Events
|
||||
const WS_EVENTS = {
|
||||
// Connection
|
||||
CONNECTED: 'connected',
|
||||
CONNECT: 'connect',
|
||||
DISCONNECT: 'disconnect',
|
||||
|
||||
// Scan events
|
||||
SCAN_STARTED: 'scan_started',
|
||||
SCAN_PROGRESS: 'scan_progress',
|
||||
SCAN_COMPLETED: 'scan_completed',
|
||||
SCAN_COMPLETE: 'scan_complete',
|
||||
SCAN_ERROR: 'scan_error',
|
||||
SCAN_FAILED: 'scan_failed',
|
||||
|
||||
// Scheduled scan events
|
||||
SCHEDULED_RESCAN_STARTED: 'scheduled_rescan_started',
|
||||
SCHEDULED_RESCAN_COMPLETED: 'scheduled_rescan_completed',
|
||||
SCHEDULED_RESCAN_ERROR: 'scheduled_rescan_error',
|
||||
SCHEDULED_RESCAN_SKIPPED: 'scheduled_rescan_skipped',
|
||||
|
||||
// Download events
|
||||
DOWNLOAD_STARTED: 'download_started',
|
||||
DOWNLOAD_PROGRESS: 'download_progress',
|
||||
DOWNLOAD_COMPLETED: 'download_completed',
|
||||
DOWNLOAD_COMPLETE: 'download_complete',
|
||||
DOWNLOAD_ERROR: 'download_error',
|
||||
DOWNLOAD_FAILED: 'download_failed',
|
||||
DOWNLOAD_PAUSED: 'download_paused',
|
||||
DOWNLOAD_RESUMED: 'download_resumed',
|
||||
DOWNLOAD_CANCELLED: 'download_cancelled',
|
||||
DOWNLOAD_STOPPED: 'download_stopped',
|
||||
DOWNLOAD_STOP_REQUESTED: 'download_stop_requested',
|
||||
|
||||
// Queue events
|
||||
QUEUE_UPDATED: 'queue_updated',
|
||||
QUEUE_STATUS: 'queue_status',
|
||||
QUEUE_STARTED: 'queue_started',
|
||||
QUEUE_STOPPED: 'queue_stopped',
|
||||
QUEUE_PAUSED: 'queue_paused',
|
||||
QUEUE_RESUMED: 'queue_resumed',
|
||||
QUEUE_COMPLETED: 'queue_completed',
|
||||
DOWNLOAD_QUEUE_COMPLETED: 'download_queue_completed',
|
||||
DOWNLOAD_QUEUE_UPDATE: 'download_queue_update',
|
||||
DOWNLOAD_EPISODE_UPDATE: 'download_episode_update',
|
||||
DOWNLOAD_SERIES_COMPLETED: 'download_series_completed',
|
||||
|
||||
// Auto download
|
||||
AUTO_DOWNLOAD_STARTED: 'auto_download_started',
|
||||
AUTO_DOWNLOAD_ERROR: 'auto_download_error'
|
||||
};
|
||||
|
||||
// Public API
|
||||
return {
|
||||
API: API,
|
||||
STORAGE_KEYS: STORAGE_KEYS,
|
||||
DEFAULTS: DEFAULTS,
|
||||
WS_ROOMS: WS_ROOMS,
|
||||
WS_EVENTS: WS_EVENTS
|
||||
};
|
||||
})();
|
||||
73
src/server/web/static/js/shared/theme.js
Normal file
73
src/server/web/static/js/shared/theme.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* AniWorld - Theme Module
|
||||
*
|
||||
* Dark/light mode management and persistence.
|
||||
*
|
||||
* Dependencies: constants.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.Theme = (function() {
|
||||
'use strict';
|
||||
|
||||
const STORAGE = AniWorld.Constants.STORAGE_KEYS;
|
||||
const DEFAULTS = AniWorld.Constants.DEFAULTS;
|
||||
|
||||
/**
|
||||
* Initialize theme from saved preference
|
||||
*/
|
||||
function init() {
|
||||
const savedTheme = localStorage.getItem(STORAGE.THEME) || DEFAULTS.THEME;
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application theme
|
||||
* @param {string} theme - 'light' or 'dark'
|
||||
*/
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(STORAGE.THEME, theme);
|
||||
|
||||
// Update theme toggle icon if it exists
|
||||
const themeIcon = document.querySelector('#theme-toggle i');
|
||||
if (themeIcon) {
|
||||
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between light and dark themes
|
||||
*/
|
||||
function toggle() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || DEFAULTS.THEME;
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theme
|
||||
* @returns {string} 'light' or 'dark'
|
||||
*/
|
||||
function getCurrentTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') || DEFAULTS.THEME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if dark mode is active
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDarkMode() {
|
||||
return getCurrentTheme() === 'dark';
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
setTheme: setTheme,
|
||||
toggle: toggle,
|
||||
getCurrentTheme: getCurrentTheme,
|
||||
isDarkMode: isDarkMode
|
||||
};
|
||||
})();
|
||||
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
|
||||
};
|
||||
})();
|
||||
164
src/server/web/static/js/shared/websocket-client.js
Normal file
164
src/server/web/static/js/shared/websocket-client.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* AniWorld - WebSocket Client Module
|
||||
*
|
||||
* WebSocket connection management and event handling.
|
||||
*
|
||||
* Dependencies: constants.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.WebSocketClient = (function() {
|
||||
'use strict';
|
||||
|
||||
const WS_EVENTS = AniWorld.Constants.WS_EVENTS;
|
||||
|
||||
let socket = null;
|
||||
let isConnected = false;
|
||||
let eventHandlers = {};
|
||||
|
||||
/**
|
||||
* Initialize WebSocket connection
|
||||
* @param {Object} handlers - Object mapping event names to handler functions
|
||||
*/
|
||||
function init(handlers) {
|
||||
handlers = handlers || {};
|
||||
eventHandlers = handlers;
|
||||
|
||||
// Check if Socket.IO is available
|
||||
if (typeof io === 'undefined') {
|
||||
console.error('Socket.IO not loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
socket = io();
|
||||
|
||||
// Handle connection events
|
||||
socket.on('connected', function(data) {
|
||||
console.log('WebSocket connection confirmed', data);
|
||||
});
|
||||
|
||||
socket.on('connect', function() {
|
||||
isConnected = true;
|
||||
console.log('Connected to server');
|
||||
|
||||
// Subscribe to rooms
|
||||
if (socket.join) {
|
||||
socket.join('scan');
|
||||
socket.join('downloads');
|
||||
socket.join('queue');
|
||||
}
|
||||
|
||||
// Call custom connect handler if provided
|
||||
if (eventHandlers.onConnect) {
|
||||
eventHandlers.onConnect();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
isConnected = false;
|
||||
console.log('Disconnected from server');
|
||||
|
||||
// Call custom disconnect handler if provided
|
||||
if (eventHandlers.onDisconnect) {
|
||||
eventHandlers.onDisconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up event handlers for common events
|
||||
setupDefaultHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up default event handlers
|
||||
*/
|
||||
function setupDefaultHandlers() {
|
||||
if (!socket) return;
|
||||
|
||||
// Register any events that have handlers
|
||||
Object.keys(eventHandlers).forEach(function(eventName) {
|
||||
if (eventName !== 'onConnect' && eventName !== 'onDisconnect') {
|
||||
socket.on(eventName, eventHandlers[eventName]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event handler
|
||||
* @param {string} eventName - The event name
|
||||
* @param {Function} handler - The handler function
|
||||
*/
|
||||
function on(eventName, handler) {
|
||||
if (!socket) {
|
||||
console.warn('Socket not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
eventHandlers[eventName] = handler;
|
||||
socket.off(eventName); // Remove existing handler
|
||||
socket.on(eventName, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event handler
|
||||
* @param {string} eventName - The event name
|
||||
*/
|
||||
function off(eventName) {
|
||||
if (!socket) return;
|
||||
|
||||
delete eventHandlers[eventName];
|
||||
socket.off(eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to the server
|
||||
* @param {string} eventName - The event name
|
||||
* @param {*} data - The data to send
|
||||
*/
|
||||
function emit(eventName, data) {
|
||||
if (!socket || !isConnected) {
|
||||
console.warn('Socket not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit(eventName, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
* @returns {boolean} True if connected
|
||||
*/
|
||||
function getConnectionStatus() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the socket instance
|
||||
* @returns {Object} The Socket.IO socket instance
|
||||
*/
|
||||
function getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from server
|
||||
*/
|
||||
function disconnect() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
on: on,
|
||||
off: off,
|
||||
emit: emit,
|
||||
isConnected: getConnectionStatus,
|
||||
getSocket: getSocket,
|
||||
disconnect: disconnect
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user