refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
74
src/server/web/static/js/index/advanced-config.js
Normal file
74
src/server/web/static/js/index/advanced-config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* AniWorld - Advanced Config Module
|
||||
*
|
||||
* Handles advanced configuration settings like concurrent downloads,
|
||||
* timeouts, and debug mode.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.AdvancedConfig = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Load advanced configuration
|
||||
*/
|
||||
async function load() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.CONFIG_SECTION + '/advanced');
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
document.getElementById('max-concurrent-downloads').value = config.max_concurrent_downloads || 3;
|
||||
document.getElementById('provider-timeout').value = config.provider_timeout || 30;
|
||||
document.getElementById('enable-debug-mode').checked = config.enable_debug_mode === true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading advanced config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save advanced configuration
|
||||
*/
|
||||
async function save() {
|
||||
try {
|
||||
const config = {
|
||||
max_concurrent_downloads: parseInt(document.getElementById('max-concurrent-downloads').value),
|
||||
provider_timeout: parseInt(document.getElementById('provider-timeout').value),
|
||||
enable_debug_mode: document.getElementById('enable-debug-mode').checked
|
||||
};
|
||||
|
||||
const response = await AniWorld.ApiClient.request(API.CONFIG_SECTION + '/advanced', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Advanced configuration saved successfully', 'success');
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving advanced config:', error);
|
||||
AniWorld.UI.showToast('Failed to save advanced configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
load: load,
|
||||
save: save
|
||||
};
|
||||
})();
|
||||
103
src/server/web/static/js/index/app-init.js
Normal file
103
src/server/web/static/js/index/app-init.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* AniWorld - Index Page Application Initializer
|
||||
*
|
||||
* Main entry point for the index page. Initializes all modules.
|
||||
*
|
||||
* Dependencies: All shared and index modules
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.IndexApp = (function() {
|
||||
'use strict';
|
||||
|
||||
let localization = null;
|
||||
|
||||
/**
|
||||
* Initialize the index page application
|
||||
*/
|
||||
async function init() {
|
||||
console.log('AniWorld Index App initializing...');
|
||||
|
||||
// Initialize localization if available
|
||||
if (typeof Localization !== 'undefined') {
|
||||
localization = new Localization();
|
||||
}
|
||||
|
||||
// Check authentication first
|
||||
const isAuthenticated = await AniWorld.Auth.checkAuth();
|
||||
if (!isAuthenticated) {
|
||||
return; // Auth module handles redirect
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
AniWorld.Theme.init();
|
||||
|
||||
// Initialize WebSocket connection
|
||||
AniWorld.WebSocketClient.init();
|
||||
|
||||
// Initialize socket event handlers for this page
|
||||
AniWorld.IndexSocketHandler.init(localization);
|
||||
|
||||
// Initialize page modules
|
||||
AniWorld.SeriesManager.init();
|
||||
AniWorld.SelectionManager.init();
|
||||
AniWorld.Search.init();
|
||||
AniWorld.ScanManager.init();
|
||||
AniWorld.ConfigManager.init();
|
||||
|
||||
// Bind global events
|
||||
bindGlobalEvents();
|
||||
|
||||
// Load initial data
|
||||
await AniWorld.SeriesManager.loadSeries();
|
||||
|
||||
console.log('AniWorld Index App initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind global event handlers
|
||||
*/
|
||||
function bindGlobalEvents() {
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
AniWorld.Theme.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
// Logout button
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', function() {
|
||||
AniWorld.Auth.logout(AniWorld.UI.showToast);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localization instance
|
||||
*/
|
||||
function getLocalization() {
|
||||
return localization;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
getLocalization: getLocalization
|
||||
};
|
||||
})();
|
||||
|
||||
// Initialize the application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
AniWorld.IndexApp.init();
|
||||
});
|
||||
|
||||
// Expose app globally for inline event handlers (backwards compatibility)
|
||||
window.app = {
|
||||
addSeries: function(link, name) {
|
||||
return AniWorld.Search.addSeries(link, name);
|
||||
}
|
||||
};
|
||||
229
src/server/web/static/js/index/config-manager.js
Normal file
229
src/server/web/static/js/index/config-manager.js
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* AniWorld - Config Manager Module
|
||||
*
|
||||
* Orchestrates configuration modal and delegates to specialized config modules.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js,
|
||||
* scheduler-config.js, logging-config.js, advanced-config.js, main-config.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.ConfigManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Initialize the config manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events
|
||||
*/
|
||||
function bindEvents() {
|
||||
// Config modal
|
||||
const configBtn = document.getElementById('config-btn');
|
||||
if (configBtn) {
|
||||
configBtn.addEventListener('click', showConfigModal);
|
||||
}
|
||||
|
||||
const closeConfig = document.getElementById('close-config');
|
||||
if (closeConfig) {
|
||||
closeConfig.addEventListener('click', hideConfigModal);
|
||||
}
|
||||
|
||||
const configModal = document.querySelector('#config-modal .modal-overlay');
|
||||
if (configModal) {
|
||||
configModal.addEventListener('click', hideConfigModal);
|
||||
}
|
||||
|
||||
// Scheduler configuration
|
||||
bindSchedulerEvents();
|
||||
|
||||
// Logging configuration
|
||||
bindLoggingEvents();
|
||||
|
||||
// Advanced configuration
|
||||
bindAdvancedEvents();
|
||||
|
||||
// Main configuration
|
||||
bindMainEvents();
|
||||
|
||||
// Status panel
|
||||
const closeStatus = document.getElementById('close-status');
|
||||
if (closeStatus) {
|
||||
closeStatus.addEventListener('click', hideStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind scheduler-related events
|
||||
*/
|
||||
function bindSchedulerEvents() {
|
||||
const schedulerEnabled = document.getElementById('scheduled-rescan-enabled');
|
||||
if (schedulerEnabled) {
|
||||
schedulerEnabled.addEventListener('change', AniWorld.SchedulerConfig.toggleTimeInput);
|
||||
}
|
||||
|
||||
const saveScheduler = document.getElementById('save-scheduler-config');
|
||||
if (saveScheduler) {
|
||||
saveScheduler.addEventListener('click', AniWorld.SchedulerConfig.save);
|
||||
}
|
||||
|
||||
const testScheduler = document.getElementById('test-scheduled-rescan');
|
||||
if (testScheduler) {
|
||||
testScheduler.addEventListener('click', AniWorld.SchedulerConfig.testRescan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind logging-related events
|
||||
*/
|
||||
function bindLoggingEvents() {
|
||||
const saveLogging = document.getElementById('save-logging-config');
|
||||
if (saveLogging) {
|
||||
saveLogging.addEventListener('click', AniWorld.LoggingConfig.save);
|
||||
}
|
||||
|
||||
const testLogging = document.getElementById('test-logging');
|
||||
if (testLogging) {
|
||||
testLogging.addEventListener('click', AniWorld.LoggingConfig.testLogging);
|
||||
}
|
||||
|
||||
const refreshLogs = document.getElementById('refresh-log-files');
|
||||
if (refreshLogs) {
|
||||
refreshLogs.addEventListener('click', AniWorld.LoggingConfig.loadLogFiles);
|
||||
}
|
||||
|
||||
const cleanupLogs = document.getElementById('cleanup-logs');
|
||||
if (cleanupLogs) {
|
||||
cleanupLogs.addEventListener('click', AniWorld.LoggingConfig.cleanupLogs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind advanced config events
|
||||
*/
|
||||
function bindAdvancedEvents() {
|
||||
const saveAdvanced = document.getElementById('save-advanced-config');
|
||||
if (saveAdvanced) {
|
||||
saveAdvanced.addEventListener('click', AniWorld.AdvancedConfig.save);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind main configuration events
|
||||
*/
|
||||
function bindMainEvents() {
|
||||
const createBackup = document.getElementById('create-config-backup');
|
||||
if (createBackup) {
|
||||
createBackup.addEventListener('click', AniWorld.MainConfig.createBackup);
|
||||
}
|
||||
|
||||
const viewBackups = document.getElementById('view-config-backups');
|
||||
if (viewBackups) {
|
||||
viewBackups.addEventListener('click', AniWorld.MainConfig.viewBackups);
|
||||
}
|
||||
|
||||
const exportConfig = document.getElementById('export-config');
|
||||
if (exportConfig) {
|
||||
exportConfig.addEventListener('click', AniWorld.MainConfig.exportConfig);
|
||||
}
|
||||
|
||||
const validateConfig = document.getElementById('validate-config');
|
||||
if (validateConfig) {
|
||||
validateConfig.addEventListener('click', AniWorld.MainConfig.validateConfig);
|
||||
}
|
||||
|
||||
const resetConfig = document.getElementById('reset-config');
|
||||
if (resetConfig) {
|
||||
resetConfig.addEventListener('click', handleResetConfig);
|
||||
}
|
||||
|
||||
const saveMain = document.getElementById('save-main-config');
|
||||
if (saveMain) {
|
||||
saveMain.addEventListener('click', AniWorld.MainConfig.save);
|
||||
}
|
||||
|
||||
const resetMain = document.getElementById('reset-main-config');
|
||||
if (resetMain) {
|
||||
resetMain.addEventListener('click', AniWorld.MainConfig.reset);
|
||||
}
|
||||
|
||||
const testConnection = document.getElementById('test-connection');
|
||||
if (testConnection) {
|
||||
testConnection.addEventListener('click', AniWorld.MainConfig.testConnection);
|
||||
}
|
||||
|
||||
const browseDirectory = document.getElementById('browse-directory');
|
||||
if (browseDirectory) {
|
||||
browseDirectory.addEventListener('click', AniWorld.MainConfig.browseDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle reset config with modal refresh
|
||||
*/
|
||||
async function handleResetConfig() {
|
||||
const success = await AniWorld.MainConfig.resetAllConfig();
|
||||
if (success) {
|
||||
setTimeout(function() {
|
||||
hideConfigModal();
|
||||
showConfigModal();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the configuration modal
|
||||
*/
|
||||
async function showConfigModal() {
|
||||
const modal = document.getElementById('config-modal');
|
||||
|
||||
try {
|
||||
// Load current status
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_STATUS);
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('anime-directory-input').value = data.directory || '';
|
||||
document.getElementById('series-count-input').value = data.series_count || '0';
|
||||
|
||||
// Load all configuration sections
|
||||
await AniWorld.SchedulerConfig.load();
|
||||
await AniWorld.LoggingConfig.load();
|
||||
await AniWorld.AdvancedConfig.load();
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading configuration:', error);
|
||||
AniWorld.UI.showToast('Failed to load configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the configuration modal
|
||||
*/
|
||||
function hideConfigModal() {
|
||||
document.getElementById('config-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide status panel
|
||||
*/
|
||||
function hideStatus() {
|
||||
document.getElementById('status-panel').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
showConfigModal: showConfigModal,
|
||||
hideConfigModal: hideConfigModal,
|
||||
hideStatus: hideStatus
|
||||
};
|
||||
})();
|
||||
278
src/server/web/static/js/index/logging-config.js
Normal file
278
src/server/web/static/js/index/logging-config.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* AniWorld - Logging Config Module
|
||||
*
|
||||
* Handles logging configuration, log file management, and log viewing.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.LoggingConfig = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Load logging configuration
|
||||
*/
|
||||
async function load() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.LOGGING_CONFIG);
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Set form values
|
||||
document.getElementById('log-level').value = config.log_level || 'INFO';
|
||||
document.getElementById('enable-console-logging').checked = config.enable_console_logging !== false;
|
||||
document.getElementById('enable-console-progress').checked = config.enable_console_progress === true;
|
||||
document.getElementById('enable-fail2ban-logging').checked = config.enable_fail2ban_logging !== false;
|
||||
|
||||
// Load log files
|
||||
await loadLogFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading logging config:', error);
|
||||
AniWorld.UI.showToast('Failed to load logging configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load log files list
|
||||
*/
|
||||
async function loadLogFiles() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.LOGGING_FILES);
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const container = document.getElementById('log-files-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (data.files.length === 0) {
|
||||
container.innerHTML = '<div class="log-file-item"><span>No log files found</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.files.forEach(function(file) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'log-file-item';
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'log-file-info';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'log-file-name';
|
||||
name.textContent = file.name;
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'log-file-details';
|
||||
details.textContent = 'Size: ' + file.size_mb + ' MB • Modified: ' + new Date(file.modified).toLocaleDateString();
|
||||
|
||||
info.appendChild(name);
|
||||
info.appendChild(details);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'log-file-actions';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'btn btn-xs btn-secondary';
|
||||
downloadBtn.innerHTML = '<i class="fas fa-download"></i>';
|
||||
downloadBtn.title = 'Download';
|
||||
downloadBtn.onclick = function() { downloadLogFile(file.name); };
|
||||
|
||||
const viewBtn = document.createElement('button');
|
||||
viewBtn.className = 'btn btn-xs btn-secondary';
|
||||
viewBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
||||
viewBtn.title = 'View Last 100 Lines';
|
||||
viewBtn.onclick = function() { viewLogFile(file.name); };
|
||||
|
||||
actions.appendChild(downloadBtn);
|
||||
actions.appendChild(viewBtn);
|
||||
|
||||
item.appendChild(info);
|
||||
item.appendChild(actions);
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading log files:', error);
|
||||
AniWorld.UI.showToast('Failed to load log files', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save logging configuration
|
||||
*/
|
||||
async function save() {
|
||||
try {
|
||||
const config = {
|
||||
log_level: document.getElementById('log-level').value,
|
||||
enable_console_logging: document.getElementById('enable-console-logging').checked,
|
||||
enable_console_progress: document.getElementById('enable-console-progress').checked,
|
||||
enable_fail2ban_logging: document.getElementById('enable-fail2ban-logging').checked
|
||||
};
|
||||
|
||||
const response = await AniWorld.ApiClient.request(API.LOGGING_CONFIG, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Logging configuration saved successfully', 'success');
|
||||
await load();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to save logging config: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving logging config:', error);
|
||||
AniWorld.UI.showToast('Failed to save logging configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test logging functionality
|
||||
*/
|
||||
async function testLogging() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.LOGGING_TEST, {});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Test messages logged successfully', 'success');
|
||||
setTimeout(loadLogFiles, 1000);
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to test logging: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing logging:', error);
|
||||
AniWorld.UI.showToast('Failed to test logging', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old log files
|
||||
*/
|
||||
async function cleanupLogs() {
|
||||
const days = prompt('Delete log files older than how many days?', '30');
|
||||
if (!days || isNaN(days) || days < 1) {
|
||||
AniWorld.UI.showToast('Invalid number of days', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.request(API.LOGGING_CLEANUP, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ days: parseInt(days) })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast(data.message, 'success');
|
||||
await loadLogFiles();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to cleanup logs: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up logs:', error);
|
||||
AniWorld.UI.showToast('Failed to cleanup logs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a log file
|
||||
*/
|
||||
function downloadLogFile(filename) {
|
||||
const link = document.createElement('a');
|
||||
link.href = '/api/logging/files/' + encodeURIComponent(filename) + '/download';
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* View a log file's last lines
|
||||
*/
|
||||
async function viewLogFile(filename) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get('/api/logging/files/' + encodeURIComponent(filename) + '/tail?lines=100');
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Create modal to show log content
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal';
|
||||
modal.style.display = 'block';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'modal-content';
|
||||
modalContent.style.maxWidth = '80%';
|
||||
modalContent.style.maxHeight = '80%';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.innerHTML = '<h3>Log File: ' + filename + '</h3><p>Showing last ' + data.showing_lines + ' of ' + data.total_lines + ' lines</p>';
|
||||
|
||||
const content = document.createElement('pre');
|
||||
content.style.maxHeight = '60vh';
|
||||
content.style.overflow = 'auto';
|
||||
content.style.backgroundColor = '#f5f5f5';
|
||||
content.style.padding = '10px';
|
||||
content.style.fontSize = '12px';
|
||||
content.textContent = data.lines.join('\n');
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.textContent = 'Close';
|
||||
closeBtn.className = 'btn btn-secondary';
|
||||
closeBtn.onclick = function() { document.body.removeChild(modal); };
|
||||
|
||||
modalContent.appendChild(header);
|
||||
modalContent.appendChild(content);
|
||||
modalContent.appendChild(closeBtn);
|
||||
modal.appendChild(modalContent);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Close on background click
|
||||
modal.onclick = function(e) {
|
||||
if (e.target === modal) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to view log file: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error viewing log file:', error);
|
||||
AniWorld.UI.showToast('Failed to view log file', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
load: load,
|
||||
loadLogFiles: loadLogFiles,
|
||||
save: save,
|
||||
testLogging: testLogging,
|
||||
cleanupLogs: cleanupLogs,
|
||||
downloadLogFile: downloadLogFile,
|
||||
viewLogFile: viewLogFile
|
||||
};
|
||||
})();
|
||||
294
src/server/web/static/js/index/main-config.js
Normal file
294
src/server/web/static/js/index/main-config.js
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* AniWorld - Main Config Module
|
||||
*
|
||||
* Handles main configuration (directory, connection) and config management
|
||||
* (backup, export, validate, reset).
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.MainConfig = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Save main configuration
|
||||
*/
|
||||
async function save() {
|
||||
try {
|
||||
const animeDirectory = document.getElementById('anime-directory-input').value.trim();
|
||||
|
||||
if (!animeDirectory) {
|
||||
AniWorld.UI.showToast('Please enter an anime directory path', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.CONFIG_DIRECTORY, {
|
||||
directory: animeDirectory
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Main configuration saved successfully', 'success');
|
||||
await refreshStatus();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to save configuration: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving main config:', error);
|
||||
AniWorld.UI.showToast('Failed to save main configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset main configuration
|
||||
*/
|
||||
function reset() {
|
||||
if (confirm('Are you sure you want to reset the main configuration? This will clear the anime directory.')) {
|
||||
document.getElementById('anime-directory-input').value = '';
|
||||
document.getElementById('series-count-input').value = '0';
|
||||
AniWorld.UI.showToast('Main configuration reset', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test network connection
|
||||
*/
|
||||
async function testConnection() {
|
||||
try {
|
||||
AniWorld.UI.showToast('Testing connection...', 'info');
|
||||
|
||||
const response = await AniWorld.ApiClient.get(API.DIAGNOSTICS_NETWORK);
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
const networkStatus = data.data;
|
||||
const connectionDiv = document.getElementById('connection-status-display');
|
||||
const statusIndicator = connectionDiv.querySelector('.status-indicator');
|
||||
const statusText = connectionDiv.querySelector('.status-text');
|
||||
|
||||
if (networkStatus.aniworld_reachable) {
|
||||
statusIndicator.className = 'status-indicator connected';
|
||||
statusText.textContent = 'Connected';
|
||||
AniWorld.UI.showToast('Connection test successful', 'success');
|
||||
} else {
|
||||
statusIndicator.className = 'status-indicator disconnected';
|
||||
statusText.textContent = 'Disconnected';
|
||||
AniWorld.UI.showToast('Connection test failed', 'error');
|
||||
}
|
||||
} else {
|
||||
AniWorld.UI.showToast('Connection test failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing connection:', error);
|
||||
AniWorld.UI.showToast('Connection test failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse for directory
|
||||
*/
|
||||
function browseDirectory() {
|
||||
const currentPath = document.getElementById('anime-directory-input').value;
|
||||
const newPath = prompt('Enter the anime directory path:', currentPath);
|
||||
|
||||
if (newPath !== null && newPath.trim() !== '') {
|
||||
document.getElementById('anime-directory-input').value = newPath.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh status display
|
||||
*/
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_STATUS);
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('anime-directory-input').value = data.directory || '';
|
||||
document.getElementById('series-count-input').value = data.series_count || '0';
|
||||
} catch (error) {
|
||||
console.error('Error refreshing status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create configuration backup
|
||||
*/
|
||||
async function createBackup() {
|
||||
const backupName = prompt('Enter backup name (optional):');
|
||||
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.request(API.CONFIG_BACKUP, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: backupName || '' })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Backup created: ' + data.filename, 'success');
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to create backup: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating backup:', error);
|
||||
AniWorld.UI.showToast('Failed to create backup', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View configuration backups
|
||||
*/
|
||||
async function viewBackups() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.CONFIG_BACKUPS);
|
||||
if (!response) return;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showBackupsModal(data.backups);
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to load backups: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading backups:', error);
|
||||
AniWorld.UI.showToast('Failed to load backups', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show backups modal
|
||||
*/
|
||||
function showBackupsModal(backups) {
|
||||
// Implementation for showing backups modal
|
||||
console.log('Backups:', backups);
|
||||
AniWorld.UI.showToast('Found ' + backups.length + ' backup(s)', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export configuration
|
||||
*/
|
||||
function exportConfig() {
|
||||
AniWorld.UI.showToast('Export configuration feature coming soon', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration
|
||||
*/
|
||||
async function validateConfig() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.request(API.CONFIG_VALIDATE, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showValidationResults(data.validation);
|
||||
} else {
|
||||
AniWorld.UI.showToast('Validation failed: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating config:', error);
|
||||
AniWorld.UI.showToast('Failed to validate configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show validation results
|
||||
*/
|
||||
function showValidationResults(validation) {
|
||||
const container = document.getElementById('validation-results');
|
||||
container.innerHTML = '';
|
||||
container.classList.remove('hidden');
|
||||
|
||||
if (validation.valid) {
|
||||
const success = document.createElement('div');
|
||||
success.className = 'validation-success';
|
||||
success.innerHTML = '<i class="fas fa-check-circle"></i> Configuration is valid!';
|
||||
container.appendChild(success);
|
||||
} else {
|
||||
const header = document.createElement('div');
|
||||
header.innerHTML = '<strong>Validation Issues Found:</strong>';
|
||||
container.appendChild(header);
|
||||
}
|
||||
|
||||
// Show errors
|
||||
validation.errors.forEach(function(error) {
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'validation-error';
|
||||
errorDiv.innerHTML = '<i class="fas fa-times-circle"></i> Error: ' + error;
|
||||
container.appendChild(errorDiv);
|
||||
});
|
||||
|
||||
// Show warnings
|
||||
validation.warnings.forEach(function(warning) {
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.className = 'validation-warning';
|
||||
warningDiv.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Warning: ' + warning;
|
||||
container.appendChild(warningDiv);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all configuration to defaults
|
||||
*/
|
||||
async function resetAllConfig() {
|
||||
if (!confirm('Are you sure you want to reset all configuration to defaults? This cannot be undone (except by restoring a backup).')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.request(API.CONFIG_RESET, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ preserve_security: true })
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Configuration reset to defaults', 'success');
|
||||
// Notify caller to reload config modal
|
||||
return true;
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to reset config: ' + data.error, 'error');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resetting config:', error);
|
||||
AniWorld.UI.showToast('Failed to reset configuration', 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
save: save,
|
||||
reset: reset,
|
||||
testConnection: testConnection,
|
||||
browseDirectory: browseDirectory,
|
||||
refreshStatus: refreshStatus,
|
||||
createBackup: createBackup,
|
||||
viewBackups: viewBackups,
|
||||
exportConfig: exportConfig,
|
||||
validateConfig: validateConfig,
|
||||
resetAllConfig: resetAllConfig
|
||||
};
|
||||
})();
|
||||
439
src/server/web/static/js/index/scan-manager.js
Normal file
439
src/server/web/static/js/index/scan-manager.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* AniWorld - Scan Manager Module
|
||||
*
|
||||
* Handles library scanning and progress overlay.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.ScanManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
const DEFAULTS = AniWorld.Constants.DEFAULTS;
|
||||
|
||||
// State
|
||||
let scanTotalItems = 0;
|
||||
let lastScanData = null;
|
||||
|
||||
/**
|
||||
* Initialize the scan manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
// Check scan status on page load
|
||||
checkActiveScanStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events
|
||||
*/
|
||||
function bindEvents() {
|
||||
const rescanBtn = document.getElementById('rescan-btn');
|
||||
if (rescanBtn) {
|
||||
rescanBtn.addEventListener('click', rescanSeries);
|
||||
}
|
||||
|
||||
// Click on rescan status indicator to reopen scan overlay
|
||||
const rescanStatus = document.getElementById('rescan-status');
|
||||
if (rescanStatus) {
|
||||
rescanStatus.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
console.log('Rescan status clicked');
|
||||
reopenScanOverlay();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a rescan of the series directory
|
||||
*/
|
||||
async function rescanSeries() {
|
||||
try {
|
||||
// Show the overlay immediately before making the API call
|
||||
showScanProgressOverlay({
|
||||
directory: 'Starting scan...',
|
||||
total_items: 0
|
||||
});
|
||||
updateProcessStatus('rescan', true);
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.ANIME_RESCAN, {});
|
||||
|
||||
if (!response) {
|
||||
removeScanProgressOverlay();
|
||||
updateProcessStatus('rescan', false);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Debug logging
|
||||
console.log('Rescan response:', data);
|
||||
|
||||
// Note: The scan progress will be updated via WebSocket events
|
||||
// The overlay will be closed when scan_completed is received
|
||||
if (data.success !== true) {
|
||||
removeScanProgressOverlay();
|
||||
updateProcessStatus('rescan', false);
|
||||
AniWorld.UI.showToast('Rescan error: ' + data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Rescan error:', error);
|
||||
removeScanProgressOverlay();
|
||||
updateProcessStatus('rescan', false);
|
||||
AniWorld.UI.showToast('Failed to start rescan', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the scan progress overlay
|
||||
* @param {Object} data - Scan started event data
|
||||
*/
|
||||
function showScanProgressOverlay(data) {
|
||||
// Remove existing overlay if present
|
||||
removeScanProgressOverlay();
|
||||
|
||||
// Store total items for progress calculation
|
||||
scanTotalItems = data?.total_items || 0;
|
||||
|
||||
// Store last scan data for reopening
|
||||
lastScanData = data;
|
||||
|
||||
// Create overlay element
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'scan-progress-overlay';
|
||||
overlay.className = 'scan-progress-overlay';
|
||||
|
||||
const totalDisplay = scanTotalItems > 0 ? scanTotalItems : '...';
|
||||
|
||||
overlay.innerHTML =
|
||||
'<div class="scan-progress-container">' +
|
||||
'<div class="scan-progress-header">' +
|
||||
'<h3>' +
|
||||
'<span class="scan-progress-spinner"></span>' +
|
||||
'<i class="fas fa-check-circle scan-completed-icon"></i>' +
|
||||
'<span class="scan-title-text">Scanning Library</span>' +
|
||||
'</h3>' +
|
||||
'</div>' +
|
||||
'<div class="scan-progress-bar-container">' +
|
||||
'<div class="scan-progress-bar" id="scan-progress-bar" style="width: 0%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="scan-progress-text" id="scan-progress-text">' +
|
||||
'<span id="scan-current-count">0</span> / <span id="scan-total-count">' + totalDisplay + '</span> directories' +
|
||||
'</div>' +
|
||||
'<div class="scan-progress-stats">' +
|
||||
'<div class="scan-stat">' +
|
||||
'<span class="scan-stat-value" id="scan-directories-count">0</span>' +
|
||||
'<span class="scan-stat-label">Scanned</span>' +
|
||||
'</div>' +
|
||||
'<div class="scan-stat">' +
|
||||
'<span class="scan-stat-value" id="scan-files-count">0</span>' +
|
||||
'<span class="scan-stat-label">Series Found</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="scan-current-directory" id="scan-current-directory">' +
|
||||
'<span class="scan-current-directory-label">Current:</span>' +
|
||||
'<span id="scan-current-path">' + AniWorld.UI.escapeHtml(data?.directory || 'Initializing...') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="scan-elapsed-time hidden" id="scan-elapsed-time">' +
|
||||
'<i class="fas fa-clock"></i>' +
|
||||
'<span id="scan-elapsed-value">0.0s</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Add click-outside-to-close handler
|
||||
overlay.addEventListener('click', function(e) {
|
||||
// Only close if clicking the overlay background, not the container
|
||||
if (e.target === overlay) {
|
||||
removeScanProgressOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger animation by adding visible class after a brief delay
|
||||
requestAnimationFrame(function() {
|
||||
overlay.classList.add('visible');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scan progress overlay
|
||||
* @param {Object} data - Scan progress event data
|
||||
*/
|
||||
function updateScanProgressOverlay(data) {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
// Update total items if provided
|
||||
if (data.total_items && data.total_items > 0) {
|
||||
scanTotalItems = data.total_items;
|
||||
const totalCount = document.getElementById('scan-total-count');
|
||||
if (totalCount) {
|
||||
totalCount.textContent = scanTotalItems;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('scan-progress-bar');
|
||||
if (progressBar && scanTotalItems > 0 && data.directories_scanned !== undefined) {
|
||||
const percentage = Math.min(100, (data.directories_scanned / scanTotalItems) * 100);
|
||||
progressBar.style.width = percentage + '%';
|
||||
}
|
||||
|
||||
// Update current/total count display
|
||||
const currentCount = document.getElementById('scan-current-count');
|
||||
if (currentCount && data.directories_scanned !== undefined) {
|
||||
currentCount.textContent = data.directories_scanned;
|
||||
}
|
||||
|
||||
// Update directories count
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
if (dirCount && data.directories_scanned !== undefined) {
|
||||
dirCount.textContent = data.directories_scanned;
|
||||
}
|
||||
|
||||
// Update files/series count
|
||||
const filesCount = document.getElementById('scan-files-count');
|
||||
if (filesCount && data.files_found !== undefined) {
|
||||
filesCount.textContent = data.files_found;
|
||||
}
|
||||
|
||||
// Update current directory (truncate if too long)
|
||||
const currentPath = document.getElementById('scan-current-path');
|
||||
if (currentPath && data.current_directory) {
|
||||
const maxLength = 50;
|
||||
let displayPath = data.current_directory;
|
||||
if (displayPath.length > maxLength) {
|
||||
displayPath = '...' + displayPath.slice(-maxLength + 3);
|
||||
}
|
||||
currentPath.textContent = displayPath;
|
||||
currentPath.title = data.current_directory;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the scan progress overlay with completion summary
|
||||
* @param {Object} data - Scan completed event data
|
||||
*/
|
||||
function hideScanProgressOverlay(data) {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
const container = overlay.querySelector('.scan-progress-container');
|
||||
if (container) {
|
||||
container.classList.add('completed');
|
||||
}
|
||||
|
||||
// Update title
|
||||
const titleText = overlay.querySelector('.scan-title-text');
|
||||
if (titleText) {
|
||||
titleText.textContent = 'Scan Complete';
|
||||
}
|
||||
|
||||
// Complete the progress bar
|
||||
const progressBar = document.getElementById('scan-progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '100%';
|
||||
}
|
||||
|
||||
// Update final stats
|
||||
if (data) {
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
if (dirCount && data.total_directories !== undefined) {
|
||||
dirCount.textContent = data.total_directories;
|
||||
}
|
||||
|
||||
const filesCount = document.getElementById('scan-files-count');
|
||||
if (filesCount && data.total_files !== undefined) {
|
||||
filesCount.textContent = data.total_files;
|
||||
}
|
||||
|
||||
// Update progress text to show final count
|
||||
const currentCount = document.getElementById('scan-current-count');
|
||||
const totalCount = document.getElementById('scan-total-count');
|
||||
if (currentCount && data.total_directories !== undefined) {
|
||||
currentCount.textContent = data.total_directories;
|
||||
}
|
||||
if (totalCount && data.total_directories !== undefined) {
|
||||
totalCount.textContent = data.total_directories;
|
||||
}
|
||||
|
||||
// Show elapsed time
|
||||
const elapsedTimeEl = document.getElementById('scan-elapsed-time');
|
||||
const elapsedValueEl = document.getElementById('scan-elapsed-value');
|
||||
if (elapsedTimeEl && elapsedValueEl && data.elapsed_seconds !== undefined) {
|
||||
elapsedValueEl.textContent = data.elapsed_seconds.toFixed(1) + 's';
|
||||
elapsedTimeEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update current directory to show completion message
|
||||
const currentPath = document.getElementById('scan-current-path');
|
||||
if (currentPath) {
|
||||
currentPath.textContent = 'Scan finished successfully';
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-dismiss after 3 seconds
|
||||
setTimeout(function() {
|
||||
removeScanProgressOverlay();
|
||||
}, DEFAULTS.SCAN_AUTO_DISMISS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the scan progress overlay from the DOM
|
||||
*/
|
||||
function removeScanProgressOverlay() {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
// Wait for fade out animation before removing
|
||||
setTimeout(function() {
|
||||
if (overlay.parentElement) {
|
||||
overlay.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen the scan progress overlay if a scan is in progress
|
||||
*/
|
||||
async function reopenScanOverlay() {
|
||||
// Check if overlay already exists
|
||||
const existingOverlay = document.getElementById('scan-progress-overlay');
|
||||
if (existingOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if scan is running via API
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_SCAN_STATUS);
|
||||
if (!response || !response.ok) {
|
||||
console.log('Could not fetch scan status');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Scan status for reopen:', data);
|
||||
|
||||
if (data.is_scanning) {
|
||||
// A scan is in progress, show the overlay
|
||||
showScanProgressOverlay({
|
||||
directory: data.directory,
|
||||
total_items: data.total_items
|
||||
});
|
||||
|
||||
// Update with current progress
|
||||
updateScanProgressOverlay({
|
||||
directories_scanned: data.directories_scanned,
|
||||
files_found: data.directories_scanned,
|
||||
current_directory: data.current_directory,
|
||||
total_items: data.total_items
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking scan status for reopen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a scan is currently in progress
|
||||
*/
|
||||
async function checkActiveScanStatus() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_SCAN_STATUS);
|
||||
if (!response || !response.ok) {
|
||||
console.log('Could not fetch scan status, response:', response?.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Scan status check result:', data);
|
||||
|
||||
if (data.is_scanning) {
|
||||
console.log('Scan is active, updating UI indicators');
|
||||
|
||||
// Update the process status indicator
|
||||
updateProcessStatus('rescan', true);
|
||||
|
||||
// Show the overlay
|
||||
showScanProgressOverlay({
|
||||
directory: data.directory,
|
||||
total_items: data.total_items
|
||||
});
|
||||
|
||||
// Update with current progress
|
||||
updateScanProgressOverlay({
|
||||
directories_scanned: data.directories_scanned,
|
||||
files_found: data.directories_scanned,
|
||||
current_directory: data.current_directory,
|
||||
total_items: data.total_items
|
||||
});
|
||||
} else {
|
||||
console.log('No active scan detected');
|
||||
updateProcessStatus('rescan', false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking scan status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update process status indicator
|
||||
* @param {string} processName - Process name (e.g., 'rescan', 'download')
|
||||
* @param {boolean} isRunning - Whether the process is running
|
||||
* @param {boolean} hasError - Whether there's an error
|
||||
*/
|
||||
function updateProcessStatus(processName, isRunning, hasError) {
|
||||
hasError = hasError || false;
|
||||
const statusElement = document.getElementById(processName + '-status');
|
||||
if (!statusElement) {
|
||||
console.warn('Process status element not found: ' + processName + '-status');
|
||||
return;
|
||||
}
|
||||
|
||||
const statusDot = statusElement.querySelector('.status-dot');
|
||||
if (!statusDot) {
|
||||
console.warn('Status dot not found in ' + processName + '-status element');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all status classes
|
||||
statusDot.classList.remove('idle', 'running', 'error');
|
||||
statusElement.classList.remove('running', 'error', 'idle');
|
||||
|
||||
// Capitalize process name for display
|
||||
const displayName = processName.charAt(0).toUpperCase() + processName.slice(1);
|
||||
|
||||
if (hasError) {
|
||||
statusDot.classList.add('error');
|
||||
statusElement.classList.add('error');
|
||||
statusElement.title = displayName + ' error - click for details';
|
||||
} else if (isRunning) {
|
||||
statusDot.classList.add('running');
|
||||
statusElement.classList.add('running');
|
||||
statusElement.title = displayName + ' is running...';
|
||||
} else {
|
||||
statusDot.classList.add('idle');
|
||||
statusElement.classList.add('idle');
|
||||
statusElement.title = displayName + ' is idle';
|
||||
}
|
||||
|
||||
console.log('Process status updated: ' + processName + ' = ' + (isRunning ? 'running' : (hasError ? 'error' : 'idle')));
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
rescanSeries: rescanSeries,
|
||||
showScanProgressOverlay: showScanProgressOverlay,
|
||||
updateScanProgressOverlay: updateScanProgressOverlay,
|
||||
hideScanProgressOverlay: hideScanProgressOverlay,
|
||||
removeScanProgressOverlay: removeScanProgressOverlay,
|
||||
reopenScanOverlay: reopenScanOverlay,
|
||||
checkActiveScanStatus: checkActiveScanStatus,
|
||||
updateProcessStatus: updateProcessStatus
|
||||
};
|
||||
})();
|
||||
124
src/server/web/static/js/index/scheduler-config.js
Normal file
124
src/server/web/static/js/index/scheduler-config.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* AniWorld - Scheduler Config Module
|
||||
*
|
||||
* Handles scheduler configuration and scheduled rescan settings.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.SchedulerConfig = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Load scheduler configuration
|
||||
*/
|
||||
async function load() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.get(API.SCHEDULER_CONFIG);
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Update UI elements
|
||||
document.getElementById('scheduled-rescan-enabled').checked = config.enabled;
|
||||
document.getElementById('scheduled-rescan-time').value = config.time || '03:00';
|
||||
document.getElementById('auto-download-after-rescan').checked = config.auto_download_after_rescan;
|
||||
|
||||
// Update status display
|
||||
document.getElementById('next-rescan-time').textContent =
|
||||
config.next_run ? new Date(config.next_run).toLocaleString() : 'Not scheduled';
|
||||
document.getElementById('last-rescan-time').textContent =
|
||||
config.last_run ? new Date(config.last_run).toLocaleString() : 'Never';
|
||||
|
||||
const statusBadge = document.getElementById('scheduler-running-status');
|
||||
statusBadge.textContent = config.is_running ? 'Running' : 'Stopped';
|
||||
statusBadge.className = 'info-value status-badge ' + (config.is_running ? 'running' : 'stopped');
|
||||
|
||||
// Enable/disable time input based on checkbox
|
||||
toggleTimeInput();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading scheduler config:', error);
|
||||
AniWorld.UI.showToast('Failed to load scheduler configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save scheduler configuration
|
||||
*/
|
||||
async function save() {
|
||||
try {
|
||||
const enabled = document.getElementById('scheduled-rescan-enabled').checked;
|
||||
const time = document.getElementById('scheduled-rescan-time').value;
|
||||
const autoDownload = document.getElementById('auto-download-after-rescan').checked;
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.SCHEDULER_CONFIG, {
|
||||
enabled: enabled,
|
||||
time: time,
|
||||
auto_download_after_rescan: autoDownload
|
||||
});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success');
|
||||
await load();
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving scheduler config:', error);
|
||||
AniWorld.UI.showToast('Failed to save scheduler configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scheduled rescan
|
||||
*/
|
||||
async function testRescan() {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.SCHEDULER_TRIGGER, {});
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
AniWorld.UI.showToast('Test rescan triggered successfully', 'success');
|
||||
} else {
|
||||
AniWorld.UI.showToast('Failed to trigger test rescan: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error triggering test rescan:', error);
|
||||
AniWorld.UI.showToast('Failed to trigger test rescan', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle scheduler time input visibility
|
||||
*/
|
||||
function toggleTimeInput() {
|
||||
const enabled = document.getElementById('scheduled-rescan-enabled').checked;
|
||||
const timeConfig = document.getElementById('rescan-time-config');
|
||||
|
||||
if (enabled) {
|
||||
timeConfig.classList.add('enabled');
|
||||
} else {
|
||||
timeConfig.classList.remove('enabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
load: load,
|
||||
save: save,
|
||||
testRescan: testRescan,
|
||||
toggleTimeInput: toggleTimeInput
|
||||
};
|
||||
})();
|
||||
156
src/server/web/static/js/index/search.js
Normal file
156
src/server/web/static/js/index/search.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* AniWorld - Search Module
|
||||
*
|
||||
* Handles anime search functionality and result display.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js, series-manager.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.Search = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
/**
|
||||
* Initialize the search module
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events
|
||||
*/
|
||||
function bindEvents() {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const clearSearch = document.getElementById('clear-search');
|
||||
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', performSearch);
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (clearSearch) {
|
||||
clearSearch.addEventListener('click', function() {
|
||||
if (searchInput) searchInput.value = '';
|
||||
hideSearchResults();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform anime search
|
||||
*/
|
||||
async function performSearch() {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const query = searchInput ? searchInput.value.trim() : '';
|
||||
|
||||
if (!query) {
|
||||
AniWorld.UI.showToast('Please enter a search term', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AniWorld.UI.showLoading();
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.ANIME_SEARCH, { query: query });
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
// Check if response is a direct array (new format) or wrapped object (legacy)
|
||||
if (Array.isArray(data)) {
|
||||
displaySearchResults(data);
|
||||
} else if (data.status === 'success') {
|
||||
displaySearchResults(data.results);
|
||||
} else {
|
||||
AniWorld.UI.showToast('Search error: ' + (data.message || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
AniWorld.UI.showToast('Search failed', 'error');
|
||||
} finally {
|
||||
AniWorld.UI.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display search results
|
||||
* @param {Array} results - Search results array
|
||||
*/
|
||||
function displaySearchResults(results) {
|
||||
const resultsContainer = document.getElementById('search-results');
|
||||
const resultsList = document.getElementById('search-results-list');
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.classList.add('hidden');
|
||||
AniWorld.UI.showToast('No search results found', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
resultsList.innerHTML = results.map(function(result) {
|
||||
const displayName = AniWorld.UI.getDisplayName(result);
|
||||
return '<div class="search-result-item">' +
|
||||
'<span class="search-result-name">' + AniWorld.UI.escapeHtml(displayName) + '</span>' +
|
||||
'<button class="btn btn-small btn-primary" ' +
|
||||
'onclick="AniWorld.Search.addSeries(\'' + AniWorld.UI.escapeHtml(result.link) + '\', \'' +
|
||||
AniWorld.UI.escapeHtml(displayName) + '\')">' +
|
||||
'<i class="fas fa-plus"></i> Add' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
resultsContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide search results
|
||||
*/
|
||||
function hideSearchResults() {
|
||||
document.getElementById('search-results').classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a series from search results
|
||||
* @param {string} link - Series link
|
||||
* @param {string} name - Series name
|
||||
*/
|
||||
async function addSeries(link, name) {
|
||||
try {
|
||||
const response = await AniWorld.ApiClient.post(API.ANIME_ADD, { link: link, name: name });
|
||||
|
||||
if (!response) return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
AniWorld.UI.showToast(data.message, 'success');
|
||||
AniWorld.SeriesManager.loadSeries();
|
||||
hideSearchResults();
|
||||
document.getElementById('search-input').value = '';
|
||||
} else {
|
||||
AniWorld.UI.showToast('Error adding series: ' + data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding series:', error);
|
||||
AniWorld.UI.showToast('Failed to add series', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
performSearch: performSearch,
|
||||
hideSearchResults: hideSearchResults,
|
||||
addSeries: addSeries
|
||||
};
|
||||
})();
|
||||
296
src/server/web/static/js/index/selection-manager.js
Normal file
296
src/server/web/static/js/index/selection-manager.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* AniWorld - Selection Manager Module
|
||||
*
|
||||
* Handles series selection for downloads.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js, series-manager.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.SelectionManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
// State
|
||||
let selectedSeries = new Set();
|
||||
|
||||
/**
|
||||
* Initialize the selection manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events
|
||||
*/
|
||||
function bindEvents() {
|
||||
const selectAllBtn = document.getElementById('select-all');
|
||||
if (selectAllBtn) {
|
||||
selectAllBtn.addEventListener('click', toggleSelectAll);
|
||||
}
|
||||
|
||||
const downloadBtn = document.getElementById('download-selected');
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', downloadSelected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle series selection
|
||||
* @param {string} key - Series key
|
||||
* @param {boolean} selected - Whether to select or deselect
|
||||
*/
|
||||
function toggleSerieSelection(key, selected) {
|
||||
// Only allow selection of series with missing episodes
|
||||
const serie = AniWorld.SeriesManager.findByKey(key);
|
||||
if (!serie || serie.missing_episodes === 0) {
|
||||
// Uncheck the checkbox if it was checked for a complete series
|
||||
const checkbox = document.querySelector('input[data-key="' + key + '"]');
|
||||
if (checkbox) checkbox.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
selectedSeries.add(key);
|
||||
} else {
|
||||
selectedSeries.delete(key);
|
||||
}
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a series is selected
|
||||
* @param {string} key - Series key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSelected(key) {
|
||||
return selectedSeries.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selection UI (buttons and card styles)
|
||||
*/
|
||||
function updateSelectionUI() {
|
||||
const downloadBtn = document.getElementById('download-selected');
|
||||
const selectAllBtn = document.getElementById('select-all');
|
||||
|
||||
// Get series that can be selected (have missing episodes)
|
||||
const selectableSeriesData = AniWorld.SeriesManager.getFilteredSeriesData().length > 0 ?
|
||||
AniWorld.SeriesManager.getFilteredSeriesData() : AniWorld.SeriesManager.getSeriesData();
|
||||
const selectableSeries = selectableSeriesData.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
const selectableKeys = selectableSeries.map(function(serie) {
|
||||
return serie.key;
|
||||
});
|
||||
|
||||
downloadBtn.disabled = selectedSeries.size === 0;
|
||||
|
||||
const allSelectableSelected = selectableKeys.every(function(key) {
|
||||
return selectedSeries.has(key);
|
||||
});
|
||||
|
||||
if (selectedSeries.size === 0) {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
|
||||
} else if (allSelectableSelected && selectableKeys.length > 0) {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-times"></i><span>Deselect All</span>';
|
||||
} else {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
|
||||
}
|
||||
|
||||
// Update card appearances
|
||||
document.querySelectorAll('.series-card').forEach(function(card) {
|
||||
const key = card.dataset.key;
|
||||
const isSelectedCard = selectedSeries.has(key);
|
||||
card.classList.toggle('selected', isSelectedCard);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle select all / deselect all
|
||||
*/
|
||||
function toggleSelectAll() {
|
||||
// Get series that can be selected (have missing episodes)
|
||||
const selectableSeriesData = AniWorld.SeriesManager.getFilteredSeriesData().length > 0 ?
|
||||
AniWorld.SeriesManager.getFilteredSeriesData() : AniWorld.SeriesManager.getSeriesData();
|
||||
const selectableSeries = selectableSeriesData.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
const selectableKeys = selectableSeries.map(function(serie) {
|
||||
return serie.key;
|
||||
});
|
||||
|
||||
const allSelectableSelected = selectableKeys.every(function(key) {
|
||||
return selectedSeries.has(key);
|
||||
});
|
||||
|
||||
if (allSelectableSelected && selectedSeries.size > 0) {
|
||||
// Deselect all selectable series
|
||||
selectableKeys.forEach(function(key) {
|
||||
selectedSeries.delete(key);
|
||||
});
|
||||
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
} else {
|
||||
// Select all selectable series
|
||||
selectableKeys.forEach(function(key) {
|
||||
selectedSeries.add(key);
|
||||
});
|
||||
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(function(cb) {
|
||||
cb.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections
|
||||
*/
|
||||
function clearSelection() {
|
||||
selectedSeries.clear();
|
||||
document.querySelectorAll('.series-checkbox').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected series
|
||||
*/
|
||||
async function downloadSelected() {
|
||||
console.log('=== downloadSelected - Using key as primary identifier ===');
|
||||
if (selectedSeries.size === 0) {
|
||||
AniWorld.UI.showToast('No series selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedKeys = Array.from(selectedSeries);
|
||||
console.log('=== Starting download for selected series ===');
|
||||
console.log('Selected keys:', selectedKeys);
|
||||
|
||||
let totalEpisodesAdded = 0;
|
||||
let failedSeries = [];
|
||||
|
||||
// For each selected series, get its missing episodes and add to queue
|
||||
for (var i = 0; i < selectedKeys.length; i++) {
|
||||
const key = selectedKeys[i];
|
||||
const serie = AniWorld.SeriesManager.findByKey(key);
|
||||
if (!serie || !serie.episodeDict) {
|
||||
console.error('Serie not found or has no episodeDict for key:', key, serie);
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!serie.key) {
|
||||
console.error('Serie missing key:', serie);
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert episodeDict format {season: [episodes]} to episode identifiers
|
||||
const episodes = [];
|
||||
Object.entries(serie.episodeDict).forEach(function(entry) {
|
||||
const season = entry[0];
|
||||
const episodeNumbers = entry[1];
|
||||
if (Array.isArray(episodeNumbers)) {
|
||||
episodeNumbers.forEach(function(episode) {
|
||||
episodes.push({
|
||||
season: parseInt(season),
|
||||
episode: episode
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (episodes.length === 0) {
|
||||
console.log('No episodes to add for serie:', serie.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use folder name as fallback if serie name is empty
|
||||
const serieName = serie.name && serie.name.trim() ? serie.name : serie.folder;
|
||||
|
||||
// Add episodes to download queue
|
||||
const requestBody = {
|
||||
serie_id: serie.key,
|
||||
serie_folder: serie.folder,
|
||||
serie_name: serieName,
|
||||
episodes: episodes,
|
||||
priority: 'NORMAL'
|
||||
};
|
||||
console.log('Sending queue add request:', requestBody);
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.QUEUE_ADD, requestBody);
|
||||
|
||||
if (!response) {
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Queue add response:', response.status, data);
|
||||
|
||||
// Log validation errors in detail
|
||||
if (data.detail && Array.isArray(data.detail)) {
|
||||
console.error('Validation errors:', JSON.stringify(data.detail, null, 2));
|
||||
}
|
||||
|
||||
if (response.ok && data.status === 'success') {
|
||||
totalEpisodesAdded += episodes.length;
|
||||
} else {
|
||||
console.error('Failed to add to queue:', data);
|
||||
failedSeries.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Show result message
|
||||
console.log('=== Download request complete ===');
|
||||
console.log('Total episodes added:', totalEpisodesAdded);
|
||||
console.log('Failed series (keys):', failedSeries);
|
||||
|
||||
if (totalEpisodesAdded > 0) {
|
||||
const message = failedSeries.length > 0
|
||||
? 'Added ' + totalEpisodesAdded + ' episode(s) to queue (' + failedSeries.length + ' series failed)'
|
||||
: 'Added ' + totalEpisodesAdded + ' episode(s) to download queue';
|
||||
AniWorld.UI.showToast(message, 'success');
|
||||
} else {
|
||||
const errorDetails = failedSeries.length > 0
|
||||
? 'Failed series (keys): ' + failedSeries.join(', ')
|
||||
: 'No episodes were added. Check browser console for details.';
|
||||
console.error('Failed to add episodes. Details:', errorDetails);
|
||||
AniWorld.UI.showToast('Failed to add episodes to queue. Check console for details.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
AniWorld.UI.showToast('Failed to start download', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected series count
|
||||
* @returns {number}
|
||||
*/
|
||||
function getSelectionCount() {
|
||||
return selectedSeries.size;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
toggleSerieSelection: toggleSerieSelection,
|
||||
isSelected: isSelected,
|
||||
updateSelectionUI: updateSelectionUI,
|
||||
toggleSelectAll: toggleSelectAll,
|
||||
clearSelection: clearSelection,
|
||||
downloadSelected: downloadSelected,
|
||||
getSelectionCount: getSelectionCount
|
||||
};
|
||||
})();
|
||||
302
src/server/web/static/js/index/series-manager.js
Normal file
302
src/server/web/static/js/index/series-manager.js
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* AniWorld - Series Manager Module
|
||||
*
|
||||
* Manages series data, filtering, sorting, and rendering.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.SeriesManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
// State
|
||||
let seriesData = [];
|
||||
let filteredSeriesData = [];
|
||||
let showMissingOnly = false;
|
||||
let sortAlphabetical = false;
|
||||
|
||||
/**
|
||||
* Initialize the series manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events for filtering and sorting
|
||||
*/
|
||||
function bindEvents() {
|
||||
const missingOnlyBtn = document.getElementById('show-missing-only');
|
||||
if (missingOnlyBtn) {
|
||||
missingOnlyBtn.addEventListener('click', toggleMissingOnlyFilter);
|
||||
}
|
||||
|
||||
const sortBtn = document.getElementById('sort-alphabetical');
|
||||
if (sortBtn) {
|
||||
sortBtn.addEventListener('click', toggleAlphabeticalSort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load series from API
|
||||
* @returns {Promise<Array>} Array of series data
|
||||
*/
|
||||
async function loadSeries() {
|
||||
try {
|
||||
AniWorld.UI.showLoading();
|
||||
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_LIST);
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if response has the expected format
|
||||
if (Array.isArray(data)) {
|
||||
// API returns array of AnimeSummary objects
|
||||
seriesData = data.map(function(anime) {
|
||||
// Count total missing episodes from the episode dictionary
|
||||
const episodeDict = anime.missing_episodes || {};
|
||||
const totalMissing = Object.values(episodeDict).reduce(
|
||||
function(sum, episodes) {
|
||||
return sum + (Array.isArray(episodes) ? episodes.length : 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
key: anime.key,
|
||||
name: anime.name,
|
||||
site: anime.site,
|
||||
folder: anime.folder,
|
||||
episodeDict: episodeDict,
|
||||
missing_episodes: totalMissing,
|
||||
has_missing: anime.has_missing || totalMissing > 0
|
||||
};
|
||||
});
|
||||
} else if (data.status === 'success') {
|
||||
// Legacy format support
|
||||
seriesData = data.series;
|
||||
} else {
|
||||
AniWorld.UI.showToast('Error loading series: ' + (data.message || 'Unknown error'), 'error');
|
||||
return [];
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
return seriesData;
|
||||
} catch (error) {
|
||||
console.error('Error loading series:', error);
|
||||
AniWorld.UI.showToast('Failed to load series', 'error');
|
||||
return [];
|
||||
} finally {
|
||||
AniWorld.UI.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle missing episodes only filter
|
||||
*/
|
||||
function toggleMissingOnlyFilter() {
|
||||
showMissingOnly = !showMissingOnly;
|
||||
const button = document.getElementById('show-missing-only');
|
||||
|
||||
button.setAttribute('data-active', showMissingOnly);
|
||||
button.classList.toggle('active', showMissingOnly);
|
||||
|
||||
const icon = button.querySelector('i');
|
||||
const text = button.querySelector('span');
|
||||
|
||||
if (showMissingOnly) {
|
||||
icon.className = 'fas fa-filter-circle-xmark';
|
||||
text.textContent = 'Show All Series';
|
||||
} else {
|
||||
icon.className = 'fas fa-filter';
|
||||
text.textContent = 'Missing Episodes Only';
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
|
||||
// Clear selection when filter changes
|
||||
if (AniWorld.SelectionManager) {
|
||||
AniWorld.SelectionManager.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle alphabetical sorting
|
||||
*/
|
||||
function toggleAlphabeticalSort() {
|
||||
sortAlphabetical = !sortAlphabetical;
|
||||
const button = document.getElementById('sort-alphabetical');
|
||||
|
||||
button.setAttribute('data-active', sortAlphabetical);
|
||||
button.classList.toggle('active', sortAlphabetical);
|
||||
|
||||
const icon = button.querySelector('i');
|
||||
const text = button.querySelector('span');
|
||||
|
||||
if (sortAlphabetical) {
|
||||
icon.className = 'fas fa-sort-alpha-up';
|
||||
text.textContent = 'Default Sort';
|
||||
} else {
|
||||
icon.className = 'fas fa-sort-alpha-down';
|
||||
text.textContent = 'A-Z Sort';
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply current filters and sorting to series data
|
||||
*/
|
||||
function applyFiltersAndSort() {
|
||||
let filtered = seriesData.slice();
|
||||
|
||||
// Sort based on the current sorting mode
|
||||
filtered.sort(function(a, b) {
|
||||
if (sortAlphabetical) {
|
||||
// Pure alphabetical sorting
|
||||
return AniWorld.UI.getDisplayName(a).localeCompare(AniWorld.UI.getDisplayName(b));
|
||||
} else {
|
||||
// Default sorting: missing episodes first (descending), then by name
|
||||
if (a.missing_episodes > 0 && b.missing_episodes === 0) return -1;
|
||||
if (a.missing_episodes === 0 && b.missing_episodes > 0) return 1;
|
||||
|
||||
// If both have missing episodes, sort by count (descending)
|
||||
if (a.missing_episodes > 0 && b.missing_episodes > 0) {
|
||||
if (a.missing_episodes !== b.missing_episodes) {
|
||||
return b.missing_episodes - a.missing_episodes;
|
||||
}
|
||||
}
|
||||
|
||||
// For series with same missing episode status, maintain stable order
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply missing episodes filter
|
||||
if (showMissingOnly) {
|
||||
filtered = filtered.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
}
|
||||
|
||||
filteredSeriesData = filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render series cards in the grid
|
||||
*/
|
||||
function renderSeries() {
|
||||
const grid = document.getElementById('series-grid');
|
||||
const dataToRender = filteredSeriesData.length > 0 ? filteredSeriesData :
|
||||
(seriesData.length > 0 ? seriesData : []);
|
||||
|
||||
if (dataToRender.length === 0) {
|
||||
const message = showMissingOnly ?
|
||||
'No series with missing episodes found.' :
|
||||
'No series found. Try searching for anime or rescanning your directory.';
|
||||
|
||||
grid.innerHTML =
|
||||
'<div class="text-center" style="grid-column: 1 / -1; padding: 2rem;">' +
|
||||
'<i class="fas fa-tv" style="font-size: 48px; color: var(--color-text-tertiary); margin-bottom: 1rem;"></i>' +
|
||||
'<p style="color: var(--color-text-secondary);">' + message + '</p>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = dataToRender.map(function(serie) {
|
||||
return createSerieCard(serie);
|
||||
}).join('');
|
||||
|
||||
// Bind checkbox events
|
||||
grid.querySelectorAll('.series-checkbox').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function(e) {
|
||||
if (AniWorld.SelectionManager) {
|
||||
AniWorld.SelectionManager.toggleSerieSelection(e.target.dataset.key, e.target.checked);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML for a series card
|
||||
* @param {Object} serie - Series data object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createSerieCard(serie) {
|
||||
const isSelected = AniWorld.SelectionManager ? AniWorld.SelectionManager.isSelected(serie.key) : false;
|
||||
const hasMissingEpisodes = serie.missing_episodes > 0;
|
||||
const canBeSelected = hasMissingEpisodes;
|
||||
|
||||
return '<div class="series-card ' + (isSelected ? 'selected' : '') + ' ' +
|
||||
(hasMissingEpisodes ? 'has-missing' : 'complete') + '" ' +
|
||||
'data-key="' + serie.key + '" data-folder="' + serie.folder + '">' +
|
||||
'<div class="series-card-header">' +
|
||||
'<input type="checkbox" class="series-checkbox" data-key="' + serie.key + '"' +
|
||||
(isSelected ? ' checked' : '') + (canBeSelected ? '' : ' disabled') + '>' +
|
||||
'<div class="series-info">' +
|
||||
'<h3>' + AniWorld.UI.escapeHtml(AniWorld.UI.getDisplayName(serie)) + '</h3>' +
|
||||
'<div class="series-folder">' + AniWorld.UI.escapeHtml(serie.folder) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="series-status">' +
|
||||
(hasMissingEpisodes ? '' : '<i class="fas fa-check-circle status-complete" title="Complete"></i>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="series-stats">' +
|
||||
'<div class="missing-episodes ' + (hasMissingEpisodes ? 'has-missing' : 'complete') + '">' +
|
||||
'<i class="fas ' + (hasMissingEpisodes ? 'fa-exclamation-triangle' : 'fa-check') + '"></i>' +
|
||||
'<span>' + (hasMissingEpisodes ? serie.missing_episodes + ' missing episodes' : 'Complete') + '</span>' +
|
||||
'</div>' +
|
||||
'<span class="series-site">' + serie.site + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all series data
|
||||
* @returns {Array} Series data array
|
||||
*/
|
||||
function getSeriesData() {
|
||||
return seriesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered series data
|
||||
* @returns {Array} Filtered series data array
|
||||
*/
|
||||
function getFilteredSeriesData() {
|
||||
return filteredSeriesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a series by key
|
||||
* @param {string} key - Series key
|
||||
* @returns {Object|undefined} Series object or undefined
|
||||
*/
|
||||
function findByKey(key) {
|
||||
return seriesData.find(function(s) {
|
||||
return s.key === key;
|
||||
});
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
loadSeries: loadSeries,
|
||||
renderSeries: renderSeries,
|
||||
applyFiltersAndSort: applyFiltersAndSort,
|
||||
getSeriesData: getSeriesData,
|
||||
getFilteredSeriesData: getFilteredSeriesData,
|
||||
findByKey: findByKey
|
||||
};
|
||||
})();
|
||||
421
src/server/web/static/js/index/socket-handler.js
Normal file
421
src/server/web/static/js/index/socket-handler.js
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* AniWorld - Socket Handler Module for Index Page
|
||||
*
|
||||
* Handles WebSocket events specific to the index page.
|
||||
*
|
||||
* Dependencies: constants.js, websocket-client.js, ui-utils.js, scan-manager.js, series-manager.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.IndexSocketHandler = (function() {
|
||||
'use strict';
|
||||
|
||||
const WS_EVENTS = AniWorld.Constants.WS_EVENTS;
|
||||
|
||||
// State
|
||||
let isDownloading = false;
|
||||
let isPaused = false;
|
||||
let localization = null;
|
||||
|
||||
/**
|
||||
* Initialize socket handler
|
||||
* @param {Object} localizationObj - Localization object
|
||||
*/
|
||||
function init(localizationObj) {
|
||||
localization = localizationObj;
|
||||
setupSocketHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized text
|
||||
*/
|
||||
function getText(key) {
|
||||
if (localization && localization.getText) {
|
||||
return localization.getText(key);
|
||||
}
|
||||
// Fallback text
|
||||
const fallbacks = {
|
||||
'connected-server': 'Connected to server',
|
||||
'disconnected-server': 'Disconnected from server',
|
||||
'download-completed': 'Download completed',
|
||||
'download-failed': 'Download failed',
|
||||
'paused': 'Paused',
|
||||
'downloading': 'Downloading...',
|
||||
'connected': 'Connected',
|
||||
'disconnected': 'Disconnected'
|
||||
};
|
||||
return fallbacks[key] || key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up WebSocket event handlers
|
||||
*/
|
||||
function setupSocketHandlers() {
|
||||
const socket = AniWorld.WebSocketClient.getSocket();
|
||||
if (!socket) {
|
||||
console.warn('Socket not available for handler setup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection events
|
||||
socket.on('connect', function() {
|
||||
AniWorld.UI.showToast(getText('connected-server'), 'success');
|
||||
updateConnectionStatus(true);
|
||||
AniWorld.ScanManager.checkActiveScanStatus();
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
AniWorld.UI.showToast(getText('disconnected-server'), 'warning');
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
// Scan events
|
||||
socket.on(WS_EVENTS.SCAN_STARTED, function(data) {
|
||||
console.log('Scan started:', data);
|
||||
AniWorld.ScanManager.showScanProgressOverlay(data);
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', true);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.SCAN_PROGRESS, function(data) {
|
||||
console.log('Scan progress:', data);
|
||||
AniWorld.ScanManager.updateScanProgressOverlay(data);
|
||||
});
|
||||
|
||||
// Handle both legacy and new scan complete events
|
||||
const handleScanComplete = function(data) {
|
||||
console.log('Scan completed:', data);
|
||||
AniWorld.ScanManager.hideScanProgressOverlay(data);
|
||||
AniWorld.UI.showToast('Scan completed successfully', 'success');
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', false);
|
||||
AniWorld.SeriesManager.loadSeries();
|
||||
};
|
||||
socket.on(WS_EVENTS.SCAN_COMPLETED, handleScanComplete);
|
||||
socket.on(WS_EVENTS.SCAN_COMPLETE, handleScanComplete);
|
||||
|
||||
// Handle scan errors
|
||||
const handleScanError = function(data) {
|
||||
AniWorld.ConfigManager.hideStatus();
|
||||
AniWorld.UI.showToast('Scan error: ' + (data.message || data.error), 'error');
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', false, true);
|
||||
};
|
||||
socket.on(WS_EVENTS.SCAN_ERROR, handleScanError);
|
||||
socket.on(WS_EVENTS.SCAN_FAILED, handleScanError);
|
||||
|
||||
// Scheduled scan events
|
||||
socket.on(WS_EVENTS.SCHEDULED_RESCAN_STARTED, function() {
|
||||
AniWorld.UI.showToast('Scheduled rescan started', 'info');
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', true);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.SCHEDULED_RESCAN_COMPLETED, function(data) {
|
||||
AniWorld.UI.showToast('Scheduled rescan completed successfully', 'success');
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', false);
|
||||
AniWorld.SeriesManager.loadSeries();
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.SCHEDULED_RESCAN_ERROR, function(data) {
|
||||
AniWorld.UI.showToast('Scheduled rescan error: ' + data.error, 'error');
|
||||
AniWorld.ScanManager.updateProcessStatus('rescan', false, true);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.SCHEDULED_RESCAN_SKIPPED, function(data) {
|
||||
AniWorld.UI.showToast('Scheduled rescan skipped: ' + data.reason, 'warning');
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.AUTO_DOWNLOAD_STARTED, function(data) {
|
||||
AniWorld.UI.showToast('Auto-download started after scheduled rescan', 'info');
|
||||
AniWorld.ScanManager.updateProcessStatus('download', true);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.AUTO_DOWNLOAD_ERROR, function(data) {
|
||||
AniWorld.UI.showToast('Auto-download error: ' + data.error, 'error');
|
||||
AniWorld.ScanManager.updateProcessStatus('download', false, true);
|
||||
});
|
||||
|
||||
// Download events
|
||||
socket.on(WS_EVENTS.DOWNLOAD_STARTED, function(data) {
|
||||
isDownloading = true;
|
||||
isPaused = false;
|
||||
AniWorld.ScanManager.updateProcessStatus('download', true);
|
||||
showDownloadQueue(data);
|
||||
showStatus('Starting download of ' + data.total_series + ' series...', true, true);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_PROGRESS, function(data) {
|
||||
let status = '';
|
||||
let percent = 0;
|
||||
|
||||
if (data.progress !== undefined) {
|
||||
percent = data.progress;
|
||||
status = 'Downloading: ' + percent.toFixed(1) + '%';
|
||||
|
||||
if (data.speed_mbps && data.speed_mbps > 0) {
|
||||
status += ' (' + data.speed_mbps.toFixed(1) + ' Mbps)';
|
||||
}
|
||||
|
||||
if (data.eta_seconds && data.eta_seconds > 0) {
|
||||
const eta = AniWorld.UI.formatETA(data.eta_seconds);
|
||||
status += ' - ETA: ' + eta;
|
||||
}
|
||||
} else if (data.total_bytes) {
|
||||
percent = ((data.downloaded_bytes || 0) / data.total_bytes * 100);
|
||||
status = 'Downloading: ' + percent.toFixed(1) + '%';
|
||||
} else if (data.downloaded_mb !== undefined) {
|
||||
status = 'Downloaded: ' + data.downloaded_mb.toFixed(1) + ' MB';
|
||||
} else {
|
||||
status = 'Downloading: ' + (data.percent || '0%');
|
||||
}
|
||||
|
||||
if (percent > 0) {
|
||||
updateProgress(percent, status);
|
||||
} else {
|
||||
updateStatus(status);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_COMPLETED, function(data) {
|
||||
isDownloading = false;
|
||||
isPaused = false;
|
||||
hideDownloadQueue();
|
||||
AniWorld.ConfigManager.hideStatus();
|
||||
AniWorld.UI.showToast(getText('download-completed'), 'success');
|
||||
AniWorld.SeriesManager.loadSeries();
|
||||
AniWorld.SelectionManager.clearSelection();
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_ERROR, function(data) {
|
||||
isDownloading = false;
|
||||
isPaused = false;
|
||||
hideDownloadQueue();
|
||||
AniWorld.ConfigManager.hideStatus();
|
||||
AniWorld.UI.showToast(getText('download-failed') + ': ' + data.message, 'error');
|
||||
});
|
||||
|
||||
// Download queue events
|
||||
socket.on(WS_EVENTS.DOWNLOAD_QUEUE_COMPLETED, function() {
|
||||
AniWorld.ScanManager.updateProcessStatus('download', false);
|
||||
AniWorld.UI.showToast('All downloads completed!', 'success');
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_STOP_REQUESTED, function() {
|
||||
AniWorld.UI.showToast('Stopping downloads...', 'info');
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_STOPPED, function() {
|
||||
AniWorld.ScanManager.updateProcessStatus('download', false);
|
||||
AniWorld.UI.showToast('Downloads stopped', 'success');
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_QUEUE_UPDATE, function(data) {
|
||||
updateDownloadQueue(data);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_EPISODE_UPDATE, function(data) {
|
||||
updateCurrentEpisode(data);
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_SERIES_COMPLETED, function(data) {
|
||||
updateDownloadProgress(data);
|
||||
});
|
||||
|
||||
// Download control events
|
||||
socket.on(WS_EVENTS.DOWNLOAD_PAUSED, function() {
|
||||
isPaused = true;
|
||||
updateStatus(getText('paused'));
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_RESUMED, function() {
|
||||
isPaused = false;
|
||||
updateStatus(getText('downloading'));
|
||||
});
|
||||
|
||||
socket.on(WS_EVENTS.DOWNLOAD_CANCELLED, function() {
|
||||
isDownloading = false;
|
||||
isPaused = false;
|
||||
hideDownloadQueue();
|
||||
AniWorld.ConfigManager.hideStatus();
|
||||
AniWorld.UI.showToast('Download cancelled', 'warning');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status display
|
||||
*/
|
||||
function updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('connection-status-display');
|
||||
if (indicator) {
|
||||
const statusIndicator = indicator.querySelector('.status-indicator');
|
||||
const statusText = indicator.querySelector('.status-text');
|
||||
|
||||
if (connected) {
|
||||
statusIndicator.classList.add('connected');
|
||||
statusText.textContent = getText('connected');
|
||||
} else {
|
||||
statusIndicator.classList.remove('connected');
|
||||
statusText.textContent = getText('disconnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show status panel
|
||||
*/
|
||||
function showStatus(message, showProgress, showControls) {
|
||||
showProgress = showProgress || false;
|
||||
showControls = showControls || false;
|
||||
|
||||
const panel = document.getElementById('status-panel');
|
||||
const messageEl = document.getElementById('status-message');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const controlsContainer = document.getElementById('download-controls');
|
||||
|
||||
messageEl.textContent = message;
|
||||
progressContainer.classList.toggle('hidden', !showProgress);
|
||||
controlsContainer.classList.toggle('hidden', !showControls);
|
||||
|
||||
if (showProgress) {
|
||||
updateProgress(0);
|
||||
}
|
||||
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status message
|
||||
*/
|
||||
function updateStatus(message) {
|
||||
document.getElementById('status-message').textContent = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress bar
|
||||
*/
|
||||
function updateProgress(percent, message) {
|
||||
const fill = document.getElementById('progress-fill');
|
||||
const text = document.getElementById('progress-text');
|
||||
|
||||
fill.style.width = percent + '%';
|
||||
text.textContent = message || percent + '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show download queue
|
||||
*/
|
||||
function showDownloadQueue(data) {
|
||||
const queueSection = document.getElementById('download-queue-section');
|
||||
const queueProgress = document.getElementById('queue-progress');
|
||||
|
||||
queueProgress.textContent = '0/' + data.total_series + ' series';
|
||||
updateDownloadQueue({
|
||||
queue: data.queue || [],
|
||||
current_downloading: null,
|
||||
stats: {
|
||||
completed_series: 0,
|
||||
total_series: data.total_series
|
||||
}
|
||||
});
|
||||
|
||||
queueSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide download queue
|
||||
*/
|
||||
function hideDownloadQueue() {
|
||||
const queueSection = document.getElementById('download-queue-section');
|
||||
const currentDownload = document.getElementById('current-download');
|
||||
|
||||
queueSection.classList.add('hidden');
|
||||
currentDownload.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download queue display
|
||||
*/
|
||||
function updateDownloadQueue(data) {
|
||||
const queueList = document.getElementById('queue-list');
|
||||
const currentDownload = document.getElementById('current-download');
|
||||
const queueProgress = document.getElementById('queue-progress');
|
||||
|
||||
// Update overall progress
|
||||
if (data.stats) {
|
||||
queueProgress.textContent = data.stats.completed_series + '/' + data.stats.total_series + ' series';
|
||||
}
|
||||
|
||||
// Update current downloading
|
||||
if (data.current_downloading) {
|
||||
currentDownload.classList.remove('hidden');
|
||||
document.getElementById('current-serie-name').textContent = AniWorld.UI.getDisplayName(data.current_downloading);
|
||||
document.getElementById('current-episode').textContent = data.current_downloading.missing_episodes + ' episodes remaining';
|
||||
} else {
|
||||
currentDownload.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Update queue list
|
||||
if (data.queue && data.queue.length > 0) {
|
||||
queueList.innerHTML = data.queue.map(function(serie, index) {
|
||||
return '<div class="queue-item">' +
|
||||
'<div class="queue-item-index">' + (index + 1) + '</div>' +
|
||||
'<div class="queue-item-name">' + AniWorld.UI.escapeHtml(AniWorld.UI.getDisplayName(serie)) + '</div>' +
|
||||
'<div class="queue-item-status">Waiting</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
} else {
|
||||
queueList.innerHTML = '<div class="queue-empty">No series in queue</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current episode display
|
||||
*/
|
||||
function updateCurrentEpisode(data) {
|
||||
const currentEpisode = document.getElementById('current-episode');
|
||||
const progressFill = document.getElementById('current-progress-fill');
|
||||
const progressText = document.getElementById('current-progress-text');
|
||||
|
||||
if (currentEpisode && data.episode) {
|
||||
currentEpisode.textContent = data.episode + ' (' + data.episode_progress + ')';
|
||||
}
|
||||
|
||||
if (data.overall_progress && progressFill && progressText) {
|
||||
const parts = data.overall_progress.split('/');
|
||||
const current = parseInt(parts[0]);
|
||||
const total = parseInt(parts[1]);
|
||||
const percent = total > 0 ? (current / total * 100).toFixed(1) : 0;
|
||||
|
||||
progressFill.style.width = percent + '%';
|
||||
progressText.textContent = percent + '%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download progress display
|
||||
*/
|
||||
function updateDownloadProgress(data) {
|
||||
const queueProgress = document.getElementById('queue-progress');
|
||||
|
||||
if (queueProgress && data.completed_series && data.total_series) {
|
||||
queueProgress.textContent = data.completed_series + '/' + data.total_series + ' series';
|
||||
}
|
||||
|
||||
AniWorld.UI.showToast('Completed: ' + data.serie, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download state
|
||||
*/
|
||||
function getDownloadState() {
|
||||
return {
|
||||
isDownloading: isDownloading,
|
||||
isPaused: isPaused
|
||||
};
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
updateConnectionStatus: updateConnectionStatus,
|
||||
getDownloadState: getDownloadState
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user