refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
296
src/server/web/static/js/index/selection-manager.js
Normal file
296
src/server/web/static/js/index/selection-manager.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* AniWorld - Selection Manager Module
|
||||
*
|
||||
* Handles series selection for downloads.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js, series-manager.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.SelectionManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
// State
|
||||
let selectedSeries = new Set();
|
||||
|
||||
/**
|
||||
* Initialize the selection manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events
|
||||
*/
|
||||
function bindEvents() {
|
||||
const selectAllBtn = document.getElementById('select-all');
|
||||
if (selectAllBtn) {
|
||||
selectAllBtn.addEventListener('click', toggleSelectAll);
|
||||
}
|
||||
|
||||
const downloadBtn = document.getElementById('download-selected');
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', downloadSelected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle series selection
|
||||
* @param {string} key - Series key
|
||||
* @param {boolean} selected - Whether to select or deselect
|
||||
*/
|
||||
function toggleSerieSelection(key, selected) {
|
||||
// Only allow selection of series with missing episodes
|
||||
const serie = AniWorld.SeriesManager.findByKey(key);
|
||||
if (!serie || serie.missing_episodes === 0) {
|
||||
// Uncheck the checkbox if it was checked for a complete series
|
||||
const checkbox = document.querySelector('input[data-key="' + key + '"]');
|
||||
if (checkbox) checkbox.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
selectedSeries.add(key);
|
||||
} else {
|
||||
selectedSeries.delete(key);
|
||||
}
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a series is selected
|
||||
* @param {string} key - Series key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSelected(key) {
|
||||
return selectedSeries.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selection UI (buttons and card styles)
|
||||
*/
|
||||
function updateSelectionUI() {
|
||||
const downloadBtn = document.getElementById('download-selected');
|
||||
const selectAllBtn = document.getElementById('select-all');
|
||||
|
||||
// Get series that can be selected (have missing episodes)
|
||||
const selectableSeriesData = AniWorld.SeriesManager.getFilteredSeriesData().length > 0 ?
|
||||
AniWorld.SeriesManager.getFilteredSeriesData() : AniWorld.SeriesManager.getSeriesData();
|
||||
const selectableSeries = selectableSeriesData.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
const selectableKeys = selectableSeries.map(function(serie) {
|
||||
return serie.key;
|
||||
});
|
||||
|
||||
downloadBtn.disabled = selectedSeries.size === 0;
|
||||
|
||||
const allSelectableSelected = selectableKeys.every(function(key) {
|
||||
return selectedSeries.has(key);
|
||||
});
|
||||
|
||||
if (selectedSeries.size === 0) {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
|
||||
} else if (allSelectableSelected && selectableKeys.length > 0) {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-times"></i><span>Deselect All</span>';
|
||||
} else {
|
||||
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
|
||||
}
|
||||
|
||||
// Update card appearances
|
||||
document.querySelectorAll('.series-card').forEach(function(card) {
|
||||
const key = card.dataset.key;
|
||||
const isSelectedCard = selectedSeries.has(key);
|
||||
card.classList.toggle('selected', isSelectedCard);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle select all / deselect all
|
||||
*/
|
||||
function toggleSelectAll() {
|
||||
// Get series that can be selected (have missing episodes)
|
||||
const selectableSeriesData = AniWorld.SeriesManager.getFilteredSeriesData().length > 0 ?
|
||||
AniWorld.SeriesManager.getFilteredSeriesData() : AniWorld.SeriesManager.getSeriesData();
|
||||
const selectableSeries = selectableSeriesData.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
const selectableKeys = selectableSeries.map(function(serie) {
|
||||
return serie.key;
|
||||
});
|
||||
|
||||
const allSelectableSelected = selectableKeys.every(function(key) {
|
||||
return selectedSeries.has(key);
|
||||
});
|
||||
|
||||
if (allSelectableSelected && selectedSeries.size > 0) {
|
||||
// Deselect all selectable series
|
||||
selectableKeys.forEach(function(key) {
|
||||
selectedSeries.delete(key);
|
||||
});
|
||||
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
} else {
|
||||
// Select all selectable series
|
||||
selectableKeys.forEach(function(key) {
|
||||
selectedSeries.add(key);
|
||||
});
|
||||
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(function(cb) {
|
||||
cb.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections
|
||||
*/
|
||||
function clearSelection() {
|
||||
selectedSeries.clear();
|
||||
document.querySelectorAll('.series-checkbox').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected series
|
||||
*/
|
||||
async function downloadSelected() {
|
||||
console.log('=== downloadSelected - Using key as primary identifier ===');
|
||||
if (selectedSeries.size === 0) {
|
||||
AniWorld.UI.showToast('No series selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedKeys = Array.from(selectedSeries);
|
||||
console.log('=== Starting download for selected series ===');
|
||||
console.log('Selected keys:', selectedKeys);
|
||||
|
||||
let totalEpisodesAdded = 0;
|
||||
let failedSeries = [];
|
||||
|
||||
// For each selected series, get its missing episodes and add to queue
|
||||
for (var i = 0; i < selectedKeys.length; i++) {
|
||||
const key = selectedKeys[i];
|
||||
const serie = AniWorld.SeriesManager.findByKey(key);
|
||||
if (!serie || !serie.episodeDict) {
|
||||
console.error('Serie not found or has no episodeDict for key:', key, serie);
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!serie.key) {
|
||||
console.error('Serie missing key:', serie);
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert episodeDict format {season: [episodes]} to episode identifiers
|
||||
const episodes = [];
|
||||
Object.entries(serie.episodeDict).forEach(function(entry) {
|
||||
const season = entry[0];
|
||||
const episodeNumbers = entry[1];
|
||||
if (Array.isArray(episodeNumbers)) {
|
||||
episodeNumbers.forEach(function(episode) {
|
||||
episodes.push({
|
||||
season: parseInt(season),
|
||||
episode: episode
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (episodes.length === 0) {
|
||||
console.log('No episodes to add for serie:', serie.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use folder name as fallback if serie name is empty
|
||||
const serieName = serie.name && serie.name.trim() ? serie.name : serie.folder;
|
||||
|
||||
// Add episodes to download queue
|
||||
const requestBody = {
|
||||
serie_id: serie.key,
|
||||
serie_folder: serie.folder,
|
||||
serie_name: serieName,
|
||||
episodes: episodes,
|
||||
priority: 'NORMAL'
|
||||
};
|
||||
console.log('Sending queue add request:', requestBody);
|
||||
|
||||
const response = await AniWorld.ApiClient.post(API.QUEUE_ADD, requestBody);
|
||||
|
||||
if (!response) {
|
||||
failedSeries.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Queue add response:', response.status, data);
|
||||
|
||||
// Log validation errors in detail
|
||||
if (data.detail && Array.isArray(data.detail)) {
|
||||
console.error('Validation errors:', JSON.stringify(data.detail, null, 2));
|
||||
}
|
||||
|
||||
if (response.ok && data.status === 'success') {
|
||||
totalEpisodesAdded += episodes.length;
|
||||
} else {
|
||||
console.error('Failed to add to queue:', data);
|
||||
failedSeries.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Show result message
|
||||
console.log('=== Download request complete ===');
|
||||
console.log('Total episodes added:', totalEpisodesAdded);
|
||||
console.log('Failed series (keys):', failedSeries);
|
||||
|
||||
if (totalEpisodesAdded > 0) {
|
||||
const message = failedSeries.length > 0
|
||||
? 'Added ' + totalEpisodesAdded + ' episode(s) to queue (' + failedSeries.length + ' series failed)'
|
||||
: 'Added ' + totalEpisodesAdded + ' episode(s) to download queue';
|
||||
AniWorld.UI.showToast(message, 'success');
|
||||
} else {
|
||||
const errorDetails = failedSeries.length > 0
|
||||
? 'Failed series (keys): ' + failedSeries.join(', ')
|
||||
: 'No episodes were added. Check browser console for details.';
|
||||
console.error('Failed to add episodes. Details:', errorDetails);
|
||||
AniWorld.UI.showToast('Failed to add episodes to queue. Check console for details.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
AniWorld.UI.showToast('Failed to start download', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected series count
|
||||
* @returns {number}
|
||||
*/
|
||||
function getSelectionCount() {
|
||||
return selectedSeries.size;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
toggleSerieSelection: toggleSerieSelection,
|
||||
isSelected: isSelected,
|
||||
updateSelectionUI: updateSelectionUI,
|
||||
toggleSelectAll: toggleSelectAll,
|
||||
clearSelection: clearSelection,
|
||||
downloadSelected: downloadSelected,
|
||||
getSelectionCount: getSelectionCount
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user