1936 lines
71 KiB
JavaScript

/**
* AniWorld Manager - Main JavaScript Application
* Implements Fluent UI design principles with modern web app functionality
*/
class AniWorldApp {
constructor() {
this.socket = null;
this.selectedSeries = new Set();
this.seriesData = [];
this.filteredSeriesData = [];
this.isConnected = false;
this.isDownloading = false;
this.isPaused = false;
this.localization = new Localization();
this.showMissingOnly = false;
this.sortAlphabetical = false;
this.init();
}
async init() {
await this.checkAuthentication();
this.initSocket();
this.bindEvents();
this.loadSeries();
this.initTheme();
this.updateConnectionStatus();
this.startProcessStatusMonitoring();
// Initialize Mobile & Accessibility features
this.initMobileAndAccessibility();
}
async checkAuthentication() {
// Don't check authentication if we're already on login or setup pages
const currentPath = window.location.pathname;
if (currentPath === '/login' || currentPath === '/setup') {
return;
}
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
if (!data.has_master_password) {
// No master password set, redirect to setup
window.location.href = '/setup';
return;
}
if (!data.authenticated) {
// Not authenticated, redirect to login
window.location.href = '/login';
return;
}
// User is authenticated, show logout button if master password is set
if (data.has_master_password) {
document.getElementById('logout-btn').style.display = 'block';
}
} catch (error) {
console.error('Authentication check failed:', error);
// On error, assume we need to login
window.location.href = '/login';
}
}
async logout() {
try {
const response = await fetch('/api/auth/logout', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
this.showToast('Logged out successfully', 'success');
setTimeout(() => {
window.location.href = '/login';
}, 1000);
} else {
this.showToast('Logout failed', 'error');
}
} catch (error) {
console.error('Logout error:', error);
this.showToast('Logout failed', 'error');
}
}
toggleMissingOnlyFilter() {
this.showMissingOnly = !this.showMissingOnly;
const button = document.getElementById('show-missing-only');
button.setAttribute('data-active', this.showMissingOnly);
button.classList.toggle('active', this.showMissingOnly);
const icon = button.querySelector('i');
const text = button.querySelector('span');
if (this.showMissingOnly) {
icon.className = 'fas fa-filter-circle-xmark';
text.textContent = 'Show All Series';
} else {
icon.className = 'fas fa-filter';
text.textContent = 'Missing Episodes Only';
}
this.applyFiltersAndSort();
this.renderSeries();
this.clearSelection(); // Clear selection when filter changes
}
toggleAlphabeticalSort() {
this.sortAlphabetical = !this.sortAlphabetical;
const button = document.getElementById('sort-alphabetical');
button.setAttribute('data-active', this.sortAlphabetical);
button.classList.toggle('active', this.sortAlphabetical);
const icon = button.querySelector('i');
const text = button.querySelector('span');
if (this.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';
}
this.applyFiltersAndSort();
this.renderSeries();
}
initSocket() {
this.socket = io();
this.socket.on('connect', () => {
this.isConnected = true;
console.log('Connected to server');
this.showToast(this.localization.getText('connected-server'), 'success');
this.updateConnectionStatus();
this.checkProcessLocks();
});
this.socket.on('disconnect', () => {
this.isConnected = false;
console.log('Disconnected from server');
this.showToast(this.localization.getText('disconnected-server'), 'warning');
this.updateConnectionStatus();
});
// Scan events
this.socket.on('scan_started', () => {
this.showStatus('Scanning series...', true);
this.updateProcessStatus('rescan', true);
});
this.socket.on('scan_progress', (data) => {
this.updateStatus(`Scanning: ${data.folder} (${data.counter})`);
});
this.socket.on('scan_completed', () => {
this.hideStatus();
this.showToast('Scan completed successfully', 'success');
this.updateProcessStatus('rescan', false);
this.loadSeries();
});
this.socket.on('scan_error', (data) => {
this.hideStatus();
this.showToast(`Scan error: ${data.message}`, 'error');
this.updateProcessStatus('rescan', false, true);
});
// Scheduled scan events
this.socket.on('scheduled_rescan_started', () => {
this.showToast('Scheduled rescan started', 'info');
this.updateProcessStatus('rescan', true);
});
this.socket.on('scheduled_rescan_completed', (data) => {
this.showToast('Scheduled rescan completed successfully', 'success');
this.updateProcessStatus('rescan', false);
this.loadSeries();
});
this.socket.on('scheduled_rescan_error', (data) => {
this.showToast(`Scheduled rescan error: ${data.error}`, 'error');
this.updateProcessStatus('rescan', false, true);
});
this.socket.on('scheduled_rescan_skipped', (data) => {
this.showToast(`Scheduled rescan skipped: ${data.reason}`, 'warning');
});
this.socket.on('auto_download_started', (data) => {
this.showToast('Auto-download started after scheduled rescan', 'info');
this.updateProcessStatus('download', true);
});
this.socket.on('auto_download_error', (data) => {
this.showToast(`Auto-download error: ${data.error}`, 'error');
this.updateProcessStatus('download', false, true);
});
// Download events
this.socket.on('download_started', (data) => {
this.isDownloading = true;
this.isPaused = false;
this.showDownloadQueue(data);
this.showStatus(`Starting download of ${data.total_series} series...`, true, true);
});
this.socket.on('download_progress', (data) => {
if (data.total_bytes) {
const percent = ((data.downloaded_bytes || 0) / data.total_bytes * 100).toFixed(1);
this.updateProgress(percent, `Downloading: ${percent}%`);
} else {
this.updateStatus(`Downloading: ${data.percent || '0%'}`);
}
});
this.socket.on('download_completed', (data) => {
this.isDownloading = false;
this.isPaused = false;
this.hideDownloadQueue();
this.hideStatus();
this.showToast(this.localization.getText('download-completed'), 'success');
this.loadSeries();
this.clearSelection();
});
this.socket.on('download_error', (data) => {
this.isDownloading = false;
this.isPaused = false;
this.hideDownloadQueue();
this.hideStatus();
this.showToast(`${this.localization.getText('download-failed')}: ${data.message}`, 'error');
});
// Download queue events
this.socket.on('download_queue_update', (data) => {
this.updateDownloadQueue(data);
});
this.socket.on('download_episode_update', (data) => {
this.updateCurrentEpisode(data);
});
this.socket.on('download_series_completed', (data) => {
this.updateDownloadProgress(data);
});
// Download control events
this.socket.on('download_paused', () => {
this.isPaused = true;
this.updateStatus(this.localization.getText('paused'));
});
this.socket.on('download_resumed', () => {
this.isPaused = false;
this.updateStatus(this.localization.getText('downloading'));
});
this.socket.on('download_cancelled', () => {
this.isDownloading = false;
this.isPaused = false;
this.hideDownloadQueue();
this.hideStatus();
this.showToast('Download cancelled', 'warning');
});
}
bindEvents() {
// Theme toggle
document.getElementById('theme-toggle').addEventListener('click', () => {
this.toggleTheme();
});
// Search functionality
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const clearSearch = document.getElementById('clear-search');
searchBtn.addEventListener('click', () => {
this.performSearch();
});
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performSearch();
}
});
clearSearch.addEventListener('click', () => {
searchInput.value = '';
this.hideSearchResults();
});
// Series management
document.getElementById('select-all').addEventListener('click', () => {
this.toggleSelectAll();
});
document.getElementById('download-selected').addEventListener('click', () => {
this.downloadSelected();
});
// Rescan
document.getElementById('rescan-btn').addEventListener('click', () => {
this.rescanSeries();
});
// Configuration modal
document.getElementById('config-btn').addEventListener('click', () => {
this.showConfigModal();
});
document.getElementById('close-config').addEventListener('click', () => {
this.hideConfigModal();
});
document.querySelector('#config-modal .modal-overlay').addEventListener('click', () => {
this.hideConfigModal();
});
// Scheduler configuration
document.getElementById('scheduled-rescan-enabled').addEventListener('change', () => {
this.toggleSchedulerTimeInput();
});
document.getElementById('save-scheduler-config').addEventListener('click', () => {
this.saveSchedulerConfig();
});
document.getElementById('test-scheduled-rescan').addEventListener('click', () => {
this.testScheduledRescan();
});
// Logging configuration
document.getElementById('save-logging-config').addEventListener('click', () => {
this.saveLoggingConfig();
});
document.getElementById('test-logging').addEventListener('click', () => {
this.testLogging();
});
document.getElementById('refresh-log-files').addEventListener('click', () => {
this.loadLogFiles();
});
document.getElementById('cleanup-logs').addEventListener('click', () => {
this.cleanupLogs();
});
// Configuration management
document.getElementById('create-config-backup').addEventListener('click', () => {
this.createConfigBackup();
});
document.getElementById('view-config-backups').addEventListener('click', () => {
this.viewConfigBackups();
});
document.getElementById('export-config').addEventListener('click', () => {
this.exportConfig();
});
document.getElementById('validate-config').addEventListener('click', () => {
this.validateConfig();
});
document.getElementById('reset-config').addEventListener('click', () => {
this.resetConfig();
});
document.getElementById('save-advanced-config').addEventListener('click', () => {
this.saveAdvancedConfig();
});
// Main configuration
document.getElementById('save-main-config').addEventListener('click', () => {
this.saveMainConfig();
});
document.getElementById('reset-main-config').addEventListener('click', () => {
this.resetMainConfig();
});
document.getElementById('test-connection').addEventListener('click', () => {
this.testConnection();
});
document.getElementById('browse-directory').addEventListener('click', () => {
this.browseDirectory();
});
// Status panel
document.getElementById('close-status').addEventListener('click', () => {
this.hideStatus();
});
// Download controls
document.getElementById('pause-download').addEventListener('click', () => {
this.pauseDownload();
});
document.getElementById('resume-download').addEventListener('click', () => {
this.resumeDownload();
});
document.getElementById('cancel-download').addEventListener('click', () => {
this.cancelDownload();
});
// Logout functionality
document.getElementById('logout-btn').addEventListener('click', () => {
this.logout();
});
// Series filtering and sorting
document.getElementById('show-missing-only').addEventListener('click', () => {
this.toggleMissingOnlyFilter();
});
document.getElementById('sort-alphabetical').addEventListener('click', () => {
this.toggleAlphabeticalSort();
});
}
initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
this.setTheme(savedTheme);
}
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const themeIcon = document.querySelector('#theme-toggle i');
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
}
async loadSeries() {
try {
this.showLoading();
const response = await fetch('/api/series');
if (response.status === 401) {
window.location.href = '/login';
return;
}
const data = await response.json();
if (data.status === 'success') {
this.seriesData = data.series;
this.applyFiltersAndSort();
this.renderSeries();
} else {
this.showToast(`Error loading series: ${data.message}`, 'error');
}
} catch (error) {
console.error('Error loading series:', error);
this.showToast('Failed to load series', 'error');
} finally {
this.hideLoading();
}
}
async makeAuthenticatedRequest(url, options = {}) {
const response = await fetch(url, options);
if (response.status === 401) {
window.location.href = '/login';
return null;
}
return response;
}
applyFiltersAndSort() {
let filtered = [...this.seriesData];
// Sort by missing episodes first (descending), then by name if alphabetical is enabled
filtered.sort((a, b) => {
// Always show series with missing episodes first
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;
}
}
// Sort alphabetically if enabled
if (this.sortAlphabetical) {
return (a.name || a.folder).localeCompare(b.name || b.folder);
}
return 0;
});
// Apply missing episodes filter
if (this.showMissingOnly) {
filtered = filtered.filter(serie => serie.missing_episodes > 0);
}
this.filteredSeriesData = filtered;
}
renderSeries() {
const grid = document.getElementById('series-grid');
const dataToRender = this.filteredSeriesData.length > 0 ? this.filteredSeriesData :
(this.seriesData.length > 0 ? this.seriesData : []);
if (dataToRender.length === 0) {
const message = this.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(serie => this.createSerieCard(serie)).join('');
// Bind checkbox events
grid.querySelectorAll('.series-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
this.toggleSerieSelection(e.target.dataset.folder, e.target.checked);
});
});
}
createSerieCard(serie) {
const isSelected = this.selectedSeries.has(serie.folder);
const hasMissingEpisodes = serie.missing_episodes > 0;
const canBeSelected = hasMissingEpisodes; // Only allow selection if has missing episodes
return `
<div class="series-card ${isSelected ? 'selected' : ''} ${hasMissingEpisodes ? 'has-missing' : 'complete'}"
data-folder="${serie.folder}">
<div class="series-card-header">
<input type="checkbox"
class="series-checkbox"
data-folder="${serie.folder}"
${isSelected ? 'checked' : ''}
${!canBeSelected ? 'disabled' : ''}>
<div class="series-info">
<h3>${this.escapeHtml(serie.name)}</h3>
<div class="series-folder">${this.escapeHtml(serie.folder)}</div>
</div>
<div class="series-status">
${hasMissingEpisodes ?
'<i class="fas fa-exclamation-triangle status-missing" title="Has missing episodes"></i>' :
'<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>
`;
}
toggleSerieSelection(folder, selected) {
// Only allow selection of series with missing episodes
const serie = this.seriesData.find(s => s.folder === folder);
if (!serie || serie.missing_episodes === 0) {
// Uncheck the checkbox if it was checked for a complete series
const checkbox = document.querySelector(`input[data-folder="${folder}"]`);
if (checkbox) checkbox.checked = false;
return;
}
if (selected) {
this.selectedSeries.add(folder);
} else {
this.selectedSeries.delete(folder);
}
this.updateSelectionUI();
}
updateSelectionUI() {
const downloadBtn = document.getElementById('download-selected');
const selectAllBtn = document.getElementById('select-all');
// Get series that can be selected (have missing episodes)
const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0);
const selectableFolders = selectableSeries.map(serie => serie.folder);
downloadBtn.disabled = this.selectedSeries.size === 0;
const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder));
if (this.selectedSeries.size === 0) {
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
} else if (allSelectableSelected && selectableFolders.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(card => {
const folder = card.dataset.folder;
const isSelected = this.selectedSeries.has(folder);
card.classList.toggle('selected', isSelected);
});
}
toggleSelectAll() {
// Get series that can be selected (have missing episodes)
const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0);
const selectableFolders = selectableSeries.map(serie => serie.folder);
const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder));
if (allSelectableSelected && this.selectedSeries.size > 0) {
// Deselect all selectable series
selectableFolders.forEach(folder => this.selectedSeries.delete(folder));
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = false);
} else {
// Select all selectable series
selectableFolders.forEach(folder => this.selectedSeries.add(folder));
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = true);
}
this.updateSelectionUI();
}
clearSelection() {
this.selectedSeries.clear();
document.querySelectorAll('.series-checkbox').forEach(cb => cb.checked = false);
this.updateSelectionUI();
}
async performSearch() {
const searchInput = document.getElementById('search-input');
const query = searchInput.value.trim();
if (!query) {
this.showToast('Please enter a search term', 'warning');
return;
}
try {
this.showLoading();
const response = await this.makeAuthenticatedRequest('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.displaySearchResults(data.results);
} else {
this.showToast(`Search error: ${data.message}`, 'error');
}
} catch (error) {
console.error('Search error:', error);
this.showToast('Search failed', 'error');
} finally {
this.hideLoading();
}
}
displaySearchResults(results) {
const resultsContainer = document.getElementById('search-results');
const resultsList = document.getElementById('search-results-list');
if (results.length === 0) {
resultsContainer.classList.add('hidden');
this.showToast('No search results found', 'warning');
return;
}
resultsList.innerHTML = results.map(result => `
<div class="search-result-item">
<span class="search-result-name">${this.escapeHtml(result.name)}</span>
<button class="btn btn-small btn-primary" onclick="app.addSeries('${this.escapeHtml(result.link)}', '${this.escapeHtml(result.name)}')">
<i class="fas fa-plus"></i>
Add
</button>
</div>
`).join('');
resultsContainer.classList.remove('hidden');
}
hideSearchResults() {
document.getElementById('search-results').classList.add('hidden');
}
async addSeries(link, name) {
try {
const response = await this.makeAuthenticatedRequest('/api/add_series', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ link, name })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast(data.message, 'success');
this.loadSeries();
this.hideSearchResults();
document.getElementById('search-input').value = '';
} else {
this.showToast(`Error adding series: ${data.message}`, 'error');
}
} catch (error) {
console.error('Error adding series:', error);
this.showToast('Failed to add series', 'error');
}
}
async downloadSelected() {
if (this.selectedSeries.size === 0) {
this.showToast('No series selected', 'warning');
return;
}
try {
const folders = Array.from(this.selectedSeries);
const response = await this.makeAuthenticatedRequest('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folders })
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download started', 'success');
} else {
this.showToast(`Download error: ${data.message}`, 'error');
}
} catch (error) {
console.error('Download error:', error);
this.showToast('Failed to start download', 'error');
}
}
async rescanSeries() {
try {
const response = await this.makeAuthenticatedRequest('/api/rescan', {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Rescan started', 'success');
} else {
this.showToast(`Rescan error: ${data.message}`, 'error');
}
} catch (error) {
console.error('Rescan error:', error);
this.showToast('Failed to start rescan', 'error');
}
}
showStatus(message, showProgress = false, 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) {
this.updateProgress(0);
}
panel.classList.remove('hidden');
}
updateStatus(message) {
document.getElementById('status-message').textContent = message;
}
updateProgress(percent, message = null) {
const fill = document.getElementById('progress-fill');
const text = document.getElementById('progress-text');
fill.style.width = `${percent}%`;
text.textContent = message || `${percent}%`;
}
hideStatus() {
document.getElementById('status-panel').classList.add('hidden');
}
showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>${this.escapeHtml(message)}</span>
<button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0; margin-left: 1rem;">
<i class="fas fa-times"></i>
</button>
</div>
`;
container.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 5000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
updateConnectionStatus() {
const indicator = document.getElementById('connection-status-display');
if (indicator) {
const statusIndicator = indicator.querySelector('.status-indicator');
const statusText = indicator.querySelector('.status-text');
if (this.isConnected) {
statusIndicator.classList.add('connected');
statusText.textContent = this.localization.getText('connected');
} else {
statusIndicator.classList.remove('connected');
statusText.textContent = this.localization.getText('disconnected');
}
}
}
updateProcessStatus(processName, isRunning, hasError = false) {
const statusElement = document.getElementById(`${processName}-status`);
if (!statusElement) return;
const statusDot = statusElement.querySelector('.status-dot');
if (!statusDot) return;
// Remove all status classes
statusDot.classList.remove('idle', 'running', 'error');
if (hasError) {
statusDot.classList.add('error');
statusElement.title = `${processName} error - click for details`;
} else if (isRunning) {
statusDot.classList.add('running');
statusElement.title = `${processName} is running...`;
} else {
statusDot.classList.add('idle');
statusElement.title = `${processName} is idle`;
}
}
async checkProcessLocks() {
try {
const response = await this.makeAuthenticatedRequest('/api/process/locks/status');
if (!response) return;
// Check if response is actually JSON and not HTML (login page)
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
console.warn('Process locks API returned non-JSON response, likely authentication issue');
return;
}
const data = await response.json();
if (data.success) {
const locks = data.locks;
this.updateProcessStatus('rescan', locks.rescan?.is_locked || false);
this.updateProcessStatus('download', locks.download?.is_locked || false);
// Update button states
const rescanBtn = document.getElementById('rescan-btn');
if (rescanBtn) {
if (locks.rescan?.is_locked) {
rescanBtn.disabled = true;
rescanBtn.querySelector('span').textContent = 'Scanning...';
} else {
rescanBtn.disabled = false;
rescanBtn.querySelector('span').textContent = 'Rescan';
}
}
}
} catch (error) {
console.error('Error checking process locks:', error);
}
}
startProcessStatusMonitoring() {
// Check process status every 5 seconds
setInterval(() => {
if (this.isConnected) {
this.checkProcessLocks();
}
}, 5000);
}
async showConfigModal() {
const modal = document.getElementById('config-modal');
try {
// Load current status
const response = await this.makeAuthenticatedRequest('/api/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 scheduler configuration
await this.loadSchedulerConfig();
// Load logging configuration
await this.loadLoggingConfig();
// Load advanced configuration
await this.loadAdvancedConfig();
modal.classList.remove('hidden');
} catch (error) {
console.error('Error loading configuration:', error);
this.showToast('Failed to load configuration', 'error');
}
}
hideConfigModal() {
document.getElementById('config-modal').classList.add('hidden');
}
async loadSchedulerConfig() {
try {
const response = await this.makeAuthenticatedRequest('/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
this.toggleSchedulerTimeInput();
}
} catch (error) {
console.error('Error loading scheduler config:', error);
this.showToast('Failed to load scheduler configuration', 'error');
}
}
async saveSchedulerConfig() {
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 this.makeAuthenticatedRequest('/api/scheduler/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: enabled,
time: time,
auto_download_after_rescan: autoDownload
})
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Scheduler configuration saved successfully', 'success');
// Reload config to update display
await this.loadSchedulerConfig();
} else {
this.showToast(`Failed to save config: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error saving scheduler config:', error);
this.showToast('Failed to save scheduler configuration', 'error');
}
}
async testScheduledRescan() {
try {
const response = await this.makeAuthenticatedRequest('/api/scheduler/trigger-rescan', {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Test rescan triggered successfully', 'success');
} else {
this.showToast(`Failed to trigger test rescan: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error triggering test rescan:', error);
this.showToast('Failed to trigger test rescan', 'error');
}
}
toggleSchedulerTimeInput() {
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');
}
}
async loadLoggingConfig() {
try {
const response = await this.makeAuthenticatedRequest('/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 this.loadLogFiles();
}
} catch (error) {
console.error('Error loading logging config:', error);
this.showToast('Failed to load logging configuration', 'error');
}
}
async loadLogFiles() {
try {
const response = await this.makeAuthenticatedRequest('/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(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 = () => this.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 = () => this.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);
this.showToast('Failed to load log files', 'error');
}
}
async saveLoggingConfig() {
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 this.makeAuthenticatedRequest('/api/logging/config', {
method: 'POST',
body: JSON.stringify(config)
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Logging configuration saved successfully', 'success');
await this.loadLoggingConfig();
} else {
this.showToast(`Failed to save logging config: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error saving logging config:', error);
this.showToast('Failed to save logging configuration', 'error');
}
}
async testLogging() {
try {
const response = await this.makeAuthenticatedRequest('/api/logging/test', {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Test messages logged successfully', 'success');
setTimeout(() => this.loadLogFiles(), 1000); // Refresh log files after a second
} else {
this.showToast(`Failed to test logging: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error testing logging:', error);
this.showToast('Failed to test logging', 'error');
}
}
async loadAdvancedConfig() {
// Placeholder for advanced configuration loading
// This method is called by showConfigModal but doesn't need to do anything special yet
console.log('Advanced configuration loaded (placeholder)');
}
async cleanupLogs() {
const days = prompt('Delete log files older than how many days?', '30');
if (!days || isNaN(days) || days < 1) {
this.showToast('Invalid number of days', 'error');
return;
}
try {
const response = await this.makeAuthenticatedRequest('/api/logging/cleanup', {
method: 'POST',
body: JSON.stringify({ days: parseInt(days) })
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast(data.message, 'success');
await this.loadLogFiles();
} else {
this.showToast(`Failed to cleanup logs: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error cleaning up logs:', error);
this.showToast('Failed to cleanup logs', 'error');
}
}
downloadLogFile(filename) {
// Create download link
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);
}
async viewLogFile(filename) {
try {
const response = await this.makeAuthenticatedRequest(`/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 = () => 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 = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
} else {
this.showToast(`Failed to view log file: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error viewing log file:', error);
this.showToast('Failed to view log file', 'error');
}
}
// Configuration Management Methods
async loadAdvancedConfig() {
try {
const response = await this.makeAuthenticatedRequest('/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);
}
}
async saveAdvancedConfig() {
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 this.makeAuthenticatedRequest('/api/config/section/advanced', {
method: 'POST',
body: JSON.stringify(config)
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Advanced configuration saved successfully', 'success');
} else {
this.showToast(`Failed to save config: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error saving advanced config:', error);
this.showToast('Failed to save advanced configuration', 'error');
}
}
// Main Configuration Methods
async saveMainConfig() {
try {
const animeDirectory = document.getElementById('anime-directory-input').value.trim();
if (!animeDirectory) {
this.showToast('Please enter an anime directory path', 'error');
return;
}
const response = await this.makeAuthenticatedRequest('/api/config/directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
directory: animeDirectory
})
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Main configuration saved successfully', 'success');
// Refresh the status to get updated series count
await this.refreshStatus();
} else {
this.showToast(`Failed to save configuration: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error saving main config:', error);
this.showToast('Failed to save main configuration', 'error');
}
}
async resetMainConfig() {
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';
this.showToast('Main configuration reset', 'info');
}
}
async testConnection() {
try {
this.showToast('Testing connection...', 'info');
const response = await this.makeAuthenticatedRequest('/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';
this.showToast('Connection test successful', 'success');
} else {
statusIndicator.className = 'status-indicator disconnected';
statusText.textContent = 'Disconnected';
this.showToast('Connection test failed', 'error');
}
} else {
this.showToast('Connection test failed', 'error');
}
} catch (error) {
console.error('Error testing connection:', error);
this.showToast('Connection test failed', 'error');
}
}
async browseDirectory() {
// This would typically open a native directory browser
// For web applications, we'll show a prompt for manual entry
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();
}
}
async refreshStatus() {
try {
const response = await this.makeAuthenticatedRequest('/api/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);
}
}
async createConfigBackup() {
const backupName = prompt('Enter backup name (optional):');
try {
const response = await this.makeAuthenticatedRequest('/api/config/backup', {
method: 'POST',
body: JSON.stringify({ name: backupName || '' })
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast(`Backup created: ${data.filename}`, 'success');
} else {
this.showToast(`Failed to create backup: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error creating backup:', error);
this.showToast('Failed to create backup', 'error');
}
}
async viewConfigBackups() {
try {
const response = await this.makeAuthenticatedRequest('/api/config/backups');
if (!response) return;
const data = await response.json();
if (data.success) {
this.showBackupsModal(data.backups);
} else {
this.showToast(`Failed to load backups: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error loading backups:', error);
this.showToast('Failed to load backups', 'error');
}
}
showBackupsModal(backups) {
// Create modal to show backups
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
modalContent.style.maxWidth = '60%';
const header = document.createElement('div');
header.innerHTML = '<h3>Configuration Backups</h3>';
const backupList = document.createElement('div');
backupList.className = 'backup-list';
if (backups.length === 0) {
backupList.innerHTML = '<div class="backup-item"><span>No backups found</span></div>';
} else {
backups.forEach(backup => {
const item = document.createElement('div');
item.className = 'backup-item';
const info = document.createElement('div');
info.className = 'backup-info';
const name = document.createElement('div');
name.className = 'backup-name';
name.textContent = backup.filename;
const details = document.createElement('div');
details.className = 'backup-details';
details.textContent = `Size: ${backup.size_kb} KB • Modified: ${backup.modified_display}`;
info.appendChild(name);
info.appendChild(details);
const actions = document.createElement('div');
actions.className = 'backup-actions';
const restoreBtn = document.createElement('button');
restoreBtn.className = 'btn btn-xs btn-primary';
restoreBtn.textContent = 'Restore';
restoreBtn.onclick = () => {
if (confirm('Are you sure you want to restore this backup? Current configuration will be overwritten.')) {
this.restoreBackup(backup.filename);
document.body.removeChild(modal);
}
};
const downloadBtn = document.createElement('button');
downloadBtn.className = 'btn btn-xs btn-secondary';
downloadBtn.textContent = 'Download';
downloadBtn.onclick = () => this.downloadBackup(backup.filename);
actions.appendChild(restoreBtn);
actions.appendChild(downloadBtn);
item.appendChild(info);
item.appendChild(actions);
backupList.appendChild(item);
});
}
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.className = 'btn btn-secondary';
closeBtn.onclick = () => document.body.removeChild(modal);
modalContent.appendChild(header);
modalContent.appendChild(backupList);
modalContent.appendChild(closeBtn);
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close on background click
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
}
async restoreBackup(filename) {
try {
const response = await this.makeAuthenticatedRequest(`/api/config/backup/${encodeURIComponent(filename)}/restore`, {
method: 'POST'
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Configuration restored successfully', 'success');
// Reload the config modal
setTimeout(() => {
this.hideConfigModal();
this.showConfigModal();
}, 1000);
} else {
this.showToast(`Failed to restore backup: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error restoring backup:', error);
this.showToast('Failed to restore backup', 'error');
}
}
downloadBackup(filename) {
const link = document.createElement('a');
link.href = `/api/config/backup/${encodeURIComponent(filename)}/download`;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async exportConfig() {
try {
const includeSensitive = confirm('Include sensitive data (passwords, salts)? Click Cancel for safe export without sensitive data.');
const response = await this.makeAuthenticatedRequest('/api/config/export', {
method: 'POST',
body: JSON.stringify({ include_sensitive: includeSensitive })
});
if (response && response.ok) {
// Handle file download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `aniworld_config_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.showToast('Configuration exported successfully', 'success');
} else {
this.showToast('Failed to export configuration', 'error');
}
} catch (error) {
console.error('Error exporting config:', error);
this.showToast('Failed to export configuration', 'error');
}
}
async validateConfig() {
try {
const response = await this.makeAuthenticatedRequest('/api/config/validate', {
method: 'POST',
body: JSON.stringify({}) // Validate current config
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showValidationResults(data.validation);
} else {
this.showToast(`Validation failed: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error validating config:', error);
this.showToast('Failed to validate configuration', 'error');
}
}
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(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(warning => {
const warningDiv = document.createElement('div');
warningDiv.className = 'validation-warning';
warningDiv.innerHTML = `<i class="fas fa-exclamation-triangle"></i> Warning: ${warning}`;
container.appendChild(warningDiv);
});
}
async resetConfig() {
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 this.makeAuthenticatedRequest('/api/config/reset', {
method: 'POST',
body: JSON.stringify({ preserve_security: true })
});
if (!response) return;
const data = await response.json();
if (data.success) {
this.showToast('Configuration reset to defaults', 'success');
// Reload the config modal
setTimeout(() => {
this.hideConfigModal();
this.showConfigModal();
}, 1000);
} else {
this.showToast(`Failed to reset config: ${data.error}`, 'error');
}
} catch (error) {
console.error('Error resetting config:', error);
this.showToast('Failed to reset configuration', 'error');
}
}
async pauseDownload() {
if (!this.isDownloading || this.isPaused) return;
try {
const response = await this.makeAuthenticatedRequest('/api/download/pause', { method: 'POST' });
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
document.getElementById('pause-download').classList.add('hidden');
document.getElementById('resume-download').classList.remove('hidden');
this.showToast('Download paused', 'warning');
} else {
this.showToast(`Pause failed: ${data.message}`, 'error');
}
} catch (error) {
console.error('Pause error:', error);
this.showToast('Failed to pause download', 'error');
}
}
async resumeDownload() {
if (!this.isDownloading || !this.isPaused) return;
try {
const response = await this.makeAuthenticatedRequest('/api/download/resume', { method: 'POST' });
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
document.getElementById('pause-download').classList.remove('hidden');
document.getElementById('resume-download').classList.add('hidden');
this.showToast('Download resumed', 'success');
} else {
this.showToast(`Resume failed: ${data.message}`, 'error');
}
} catch (error) {
console.error('Resume error:', error);
this.showToast('Failed to resume download', 'error');
}
}
async cancelDownload() {
if (!this.isDownloading) return;
if (confirm('Are you sure you want to cancel the download?')) {
try {
const response = await this.makeAuthenticatedRequest('/api/download/cancel', { method: 'POST' });
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
this.showToast('Download cancelled', 'warning');
} else {
this.showToast(`Cancel failed: ${data.message}`, 'error');
}
} catch (error) {
console.error('Cancel error:', error);
this.showToast('Failed to cancel download', 'error');
}
}
}
showDownloadQueue(data) {
const queueSection = document.getElementById('download-queue-section');
const queueProgress = document.getElementById('queue-progress');
queueProgress.textContent = `0/${data.total_series} series`;
this.updateDownloadQueue({
queue: data.queue || [],
current_downloading: null,
stats: {
completed_series: 0,
total_series: data.total_series
}
});
queueSection.classList.remove('hidden');
}
hideDownloadQueue() {
const queueSection = document.getElementById('download-queue-section');
const currentDownload = document.getElementById('current-download');
queueSection.classList.add('hidden');
currentDownload.classList.add('hidden');
}
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 = data.current_downloading.name;
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((serie, index) => `
<div class="queue-item">
<div class="queue-item-index">${index + 1}</div>
<div class="queue-item-name">${this.escapeHtml(serie.name)}</div>
<div class="queue-item-status">Waiting</div>
</div>
`).join('');
} else {
queueList.innerHTML = '<div class="queue-empty">No series in queue</div>';
}
}
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})`;
}
// Update mini progress bar based on overall progress
if (data.overall_progress && progressFill && progressText) {
const [current, total] = data.overall_progress.split('/').map(n => parseInt(n));
const percent = total > 0 ? (current / total * 100).toFixed(1) : 0;
progressFill.style.width = `${percent}%`;
progressText.textContent = `${percent}%`;
}
}
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`;
}
this.showToast(`Completed: ${data.serie}`, 'success');
}
initMobileAndAccessibility() {
// Initialize Mobile Responsive Manager
if (typeof MobileResponsiveManager !== 'undefined') {
this.mobileResponsive = new MobileResponsiveManager();
}
// Initialize Touch Gesture Manager
if (typeof TouchGestureManager !== 'undefined') {
this.touchGestures = new TouchGestureManager();
}
// Initialize Accessibility Manager
if (typeof AccessibilityManager !== 'undefined') {
this.accessibility = new AccessibilityManager();
}
// Initialize Screen Reader Manager
if (typeof ScreenReaderManager !== 'undefined') {
this.screenReader = new ScreenReaderManager();
}
// Initialize Color Contrast Manager
if (typeof ColorContrastManager !== 'undefined') {
this.colorContrast = new ColorContrastManager();
}
// Initialize Multi-Screen Manager
if (typeof MultiScreenManager !== 'undefined') {
this.multiScreen = new MultiScreenManager();
}
console.log('Mobile & Accessibility features initialized');
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.app = new AniWorldApp();
});
// Global functions for inline event handlers
window.app = null;