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

${message}

`; 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 `

${this.escapeHtml(serie.name)}

${this.escapeHtml(serie.folder)}
${hasMissingEpisodes ? '' : '' }
${hasMissingEpisodes ? `${serie.missing_episodes} missing episodes` : 'Complete'}
${serie.site}
`; } 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 = 'Select All'; } else if (allSelectableSelected && selectableFolders.length > 0) { selectAllBtn.innerHTML = 'Deselect All'; } else { selectAllBtn.innerHTML = 'Select All'; } // 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 => `
${this.escapeHtml(result.name)}
`).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 = `
${this.escapeHtml(message)}
`; 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; 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-display').textContent = data.directory || 'Not configured'; document.getElementById('series-count-display').textContent = 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 = '
No log files found
'; 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 = ''; downloadBtn.title = 'Download'; downloadBtn.onclick = () => this.downloadLogFile(file.name); const viewBtn = document.createElement('button'); viewBtn.className = 'btn btn-xs btn-secondary'; viewBtn.innerHTML = ''; 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 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 = `

Log File: ${filename}

Showing last ${data.showing_lines} of ${data.total_lines} lines

`; 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'); } } 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 = '

Configuration Backups

'; const backupList = document.createElement('div'); backupList.className = 'backup-list'; if (backups.length === 0) { backupList.innerHTML = '
No backups found
'; } 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 = ' Configuration is valid!'; container.appendChild(success); } else { const header = document.createElement('div'); header.innerHTML = 'Validation Issues Found:'; container.appendChild(header); } // Show errors validation.errors.forEach(error => { const errorDiv = document.createElement('div'); errorDiv.className = 'validation-error'; errorDiv.innerHTML = ` Error: ${error}`; container.appendChild(errorDiv); }); // Show warnings validation.warnings.forEach(warning => { const warningDiv = document.createElement('div'); warningDiv.className = 'validation-warning'; warningDiv.innerHTML = ` 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) => `
${index + 1}
${this.escapeHtml(serie.name)}
Waiting
`).join(''); } else { queueList.innerHTML = '
No series in queue
'; } } 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;