1969 lines
73 KiB
JavaScript
1969 lines
73 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 based on the current sorting mode
|
|
filtered.sort((a, b) => {
|
|
if (this.sortAlphabetical) {
|
|
// Pure alphabetical sorting when A-Z is enabled
|
|
return (a.name || a.folder).localeCompare(b.name || b.folder);
|
|
} else {
|
|
// Default sorting: missing episodes first (descending), then by name
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// For series with same missing episode status, maintain stable order
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// Apply missing episodes filter
|
|
if (this.showMissingOnly) {
|
|
filtered = filtered.filter(serie => serie.missing_episodes > 0);
|
|
}
|
|
|
|
this.filteredSeriesData = filtered;
|
|
this.renderSeries();
|
|
}
|
|
|
|
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-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 from both dot and element
|
|
statusDot.classList.remove('idle', 'running', 'error');
|
|
statusElement.classList.remove('running', 'error', 'idle');
|
|
|
|
// Capitalize process name for display
|
|
const displayName = processName.charAt(0).toUpperCase() + processName.slice(1);
|
|
|
|
if (hasError) {
|
|
statusDot.classList.add('error');
|
|
statusElement.classList.add('error');
|
|
statusElement.title = `${displayName} error - click for details`;
|
|
} else if (isRunning) {
|
|
statusDot.classList.add('running');
|
|
statusElement.classList.add('running');
|
|
statusElement.title = `${displayName} is running...`;
|
|
} else {
|
|
statusDot.classList.add('idle');
|
|
statusElement.classList.add('idle');
|
|
statusElement.title = `${displayName} is idle`;
|
|
}
|
|
}
|
|
|
|
async checkProcessLocks() {
|
|
try {
|
|
const response = await this.makeAuthenticatedRequest('/api/process/locks/status');
|
|
if (!response) {
|
|
// If no response, set status as idle
|
|
this.updateProcessStatus('rescan', false);
|
|
this.updateProcessStatus('download', false);
|
|
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');
|
|
// Set status as idle if we can't get proper response
|
|
this.updateProcessStatus('rescan', false);
|
|
this.updateProcessStatus('download', false);
|
|
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;
|
|
const span = rescanBtn.querySelector('span');
|
|
if (span) span.textContent = 'Scanning...';
|
|
} else {
|
|
rescanBtn.disabled = false;
|
|
const span = rescanBtn.querySelector('span');
|
|
if (span) span.textContent = 'Rescan';
|
|
}
|
|
}
|
|
} else {
|
|
// If API returns error, set status as idle
|
|
console.warn('Process locks API returned error:', data.error);
|
|
this.updateProcessStatus('rescan', false);
|
|
this.updateProcessStatus('download', false);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking process locks:', error);
|
|
// On error, set status as idle
|
|
this.updateProcessStatus('rescan', false);
|
|
this.updateProcessStatus('download', false);
|
|
}
|
|
}
|
|
|
|
startProcessStatusMonitoring() {
|
|
// Initial check on page load
|
|
this.checkProcessLocks();
|
|
|
|
// Check process status every 5 seconds
|
|
setInterval(() => {
|
|
if (this.isConnected) {
|
|
this.checkProcessLocks();
|
|
}
|
|
}, 5000);
|
|
|
|
console.log('Process status monitoring started');
|
|
}
|
|
|
|
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; |