refactor: split CSS and JS into modular files (SRP)
This commit is contained in:
302
src/server/web/static/js/index/series-manager.js
Normal file
302
src/server/web/static/js/index/series-manager.js
Normal file
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* AniWorld - Series Manager Module
|
||||
*
|
||||
* Manages series data, filtering, sorting, and rendering.
|
||||
*
|
||||
* Dependencies: constants.js, api-client.js, ui-utils.js
|
||||
*/
|
||||
|
||||
var AniWorld = window.AniWorld || {};
|
||||
|
||||
AniWorld.SeriesManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const API = AniWorld.Constants.API;
|
||||
|
||||
// State
|
||||
let seriesData = [];
|
||||
let filteredSeriesData = [];
|
||||
let showMissingOnly = false;
|
||||
let sortAlphabetical = false;
|
||||
|
||||
/**
|
||||
* Initialize the series manager
|
||||
*/
|
||||
function init() {
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI events for filtering and sorting
|
||||
*/
|
||||
function bindEvents() {
|
||||
const missingOnlyBtn = document.getElementById('show-missing-only');
|
||||
if (missingOnlyBtn) {
|
||||
missingOnlyBtn.addEventListener('click', toggleMissingOnlyFilter);
|
||||
}
|
||||
|
||||
const sortBtn = document.getElementById('sort-alphabetical');
|
||||
if (sortBtn) {
|
||||
sortBtn.addEventListener('click', toggleAlphabeticalSort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load series from API
|
||||
* @returns {Promise<Array>} Array of series data
|
||||
*/
|
||||
async function loadSeries() {
|
||||
try {
|
||||
AniWorld.UI.showLoading();
|
||||
|
||||
const response = await AniWorld.ApiClient.get(API.ANIME_LIST);
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if response has the expected format
|
||||
if (Array.isArray(data)) {
|
||||
// API returns array of AnimeSummary objects
|
||||
seriesData = data.map(function(anime) {
|
||||
// Count total missing episodes from the episode dictionary
|
||||
const episodeDict = anime.missing_episodes || {};
|
||||
const totalMissing = Object.values(episodeDict).reduce(
|
||||
function(sum, episodes) {
|
||||
return sum + (Array.isArray(episodes) ? episodes.length : 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
key: anime.key,
|
||||
name: anime.name,
|
||||
site: anime.site,
|
||||
folder: anime.folder,
|
||||
episodeDict: episodeDict,
|
||||
missing_episodes: totalMissing,
|
||||
has_missing: anime.has_missing || totalMissing > 0
|
||||
};
|
||||
});
|
||||
} else if (data.status === 'success') {
|
||||
// Legacy format support
|
||||
seriesData = data.series;
|
||||
} else {
|
||||
AniWorld.UI.showToast('Error loading series: ' + (data.message || 'Unknown error'), 'error');
|
||||
return [];
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
return seriesData;
|
||||
} catch (error) {
|
||||
console.error('Error loading series:', error);
|
||||
AniWorld.UI.showToast('Failed to load series', 'error');
|
||||
return [];
|
||||
} finally {
|
||||
AniWorld.UI.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle missing episodes only filter
|
||||
*/
|
||||
function toggleMissingOnlyFilter() {
|
||||
showMissingOnly = !showMissingOnly;
|
||||
const button = document.getElementById('show-missing-only');
|
||||
|
||||
button.setAttribute('data-active', showMissingOnly);
|
||||
button.classList.toggle('active', showMissingOnly);
|
||||
|
||||
const icon = button.querySelector('i');
|
||||
const text = button.querySelector('span');
|
||||
|
||||
if (showMissingOnly) {
|
||||
icon.className = 'fas fa-filter-circle-xmark';
|
||||
text.textContent = 'Show All Series';
|
||||
} else {
|
||||
icon.className = 'fas fa-filter';
|
||||
text.textContent = 'Missing Episodes Only';
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
|
||||
// Clear selection when filter changes
|
||||
if (AniWorld.SelectionManager) {
|
||||
AniWorld.SelectionManager.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle alphabetical sorting
|
||||
*/
|
||||
function toggleAlphabeticalSort() {
|
||||
sortAlphabetical = !sortAlphabetical;
|
||||
const button = document.getElementById('sort-alphabetical');
|
||||
|
||||
button.setAttribute('data-active', sortAlphabetical);
|
||||
button.classList.toggle('active', sortAlphabetical);
|
||||
|
||||
const icon = button.querySelector('i');
|
||||
const text = button.querySelector('span');
|
||||
|
||||
if (sortAlphabetical) {
|
||||
icon.className = 'fas fa-sort-alpha-up';
|
||||
text.textContent = 'Default Sort';
|
||||
} else {
|
||||
icon.className = 'fas fa-sort-alpha-down';
|
||||
text.textContent = 'A-Z Sort';
|
||||
}
|
||||
|
||||
applyFiltersAndSort();
|
||||
renderSeries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply current filters and sorting to series data
|
||||
*/
|
||||
function applyFiltersAndSort() {
|
||||
let filtered = seriesData.slice();
|
||||
|
||||
// Sort based on the current sorting mode
|
||||
filtered.sort(function(a, b) {
|
||||
if (sortAlphabetical) {
|
||||
// Pure alphabetical sorting
|
||||
return AniWorld.UI.getDisplayName(a).localeCompare(AniWorld.UI.getDisplayName(b));
|
||||
} else {
|
||||
// Default sorting: missing episodes first (descending), then by name
|
||||
if (a.missing_episodes > 0 && b.missing_episodes === 0) return -1;
|
||||
if (a.missing_episodes === 0 && b.missing_episodes > 0) return 1;
|
||||
|
||||
// If both have missing episodes, sort by count (descending)
|
||||
if (a.missing_episodes > 0 && b.missing_episodes > 0) {
|
||||
if (a.missing_episodes !== b.missing_episodes) {
|
||||
return b.missing_episodes - a.missing_episodes;
|
||||
}
|
||||
}
|
||||
|
||||
// For series with same missing episode status, maintain stable order
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply missing episodes filter
|
||||
if (showMissingOnly) {
|
||||
filtered = filtered.filter(function(serie) {
|
||||
return serie.missing_episodes > 0;
|
||||
});
|
||||
}
|
||||
|
||||
filteredSeriesData = filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render series cards in the grid
|
||||
*/
|
||||
function renderSeries() {
|
||||
const grid = document.getElementById('series-grid');
|
||||
const dataToRender = filteredSeriesData.length > 0 ? filteredSeriesData :
|
||||
(seriesData.length > 0 ? seriesData : []);
|
||||
|
||||
if (dataToRender.length === 0) {
|
||||
const message = showMissingOnly ?
|
||||
'No series with missing episodes found.' :
|
||||
'No series found. Try searching for anime or rescanning your directory.';
|
||||
|
||||
grid.innerHTML =
|
||||
'<div class="text-center" style="grid-column: 1 / -1; padding: 2rem;">' +
|
||||
'<i class="fas fa-tv" style="font-size: 48px; color: var(--color-text-tertiary); margin-bottom: 1rem;"></i>' +
|
||||
'<p style="color: var(--color-text-secondary);">' + message + '</p>' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = dataToRender.map(function(serie) {
|
||||
return createSerieCard(serie);
|
||||
}).join('');
|
||||
|
||||
// Bind checkbox events
|
||||
grid.querySelectorAll('.series-checkbox').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function(e) {
|
||||
if (AniWorld.SelectionManager) {
|
||||
AniWorld.SelectionManager.toggleSerieSelection(e.target.dataset.key, e.target.checked);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML for a series card
|
||||
* @param {Object} serie - Series data object
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
function createSerieCard(serie) {
|
||||
const isSelected = AniWorld.SelectionManager ? AniWorld.SelectionManager.isSelected(serie.key) : false;
|
||||
const hasMissingEpisodes = serie.missing_episodes > 0;
|
||||
const canBeSelected = hasMissingEpisodes;
|
||||
|
||||
return '<div class="series-card ' + (isSelected ? 'selected' : '') + ' ' +
|
||||
(hasMissingEpisodes ? 'has-missing' : 'complete') + '" ' +
|
||||
'data-key="' + serie.key + '" data-folder="' + serie.folder + '">' +
|
||||
'<div class="series-card-header">' +
|
||||
'<input type="checkbox" class="series-checkbox" data-key="' + serie.key + '"' +
|
||||
(isSelected ? ' checked' : '') + (canBeSelected ? '' : ' disabled') + '>' +
|
||||
'<div class="series-info">' +
|
||||
'<h3>' + AniWorld.UI.escapeHtml(AniWorld.UI.getDisplayName(serie)) + '</h3>' +
|
||||
'<div class="series-folder">' + AniWorld.UI.escapeHtml(serie.folder) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="series-status">' +
|
||||
(hasMissingEpisodes ? '' : '<i class="fas fa-check-circle status-complete" title="Complete"></i>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="series-stats">' +
|
||||
'<div class="missing-episodes ' + (hasMissingEpisodes ? 'has-missing' : 'complete') + '">' +
|
||||
'<i class="fas ' + (hasMissingEpisodes ? 'fa-exclamation-triangle' : 'fa-check') + '"></i>' +
|
||||
'<span>' + (hasMissingEpisodes ? serie.missing_episodes + ' missing episodes' : 'Complete') + '</span>' +
|
||||
'</div>' +
|
||||
'<span class="series-site">' + serie.site + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all series data
|
||||
* @returns {Array} Series data array
|
||||
*/
|
||||
function getSeriesData() {
|
||||
return seriesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered series data
|
||||
* @returns {Array} Filtered series data array
|
||||
*/
|
||||
function getFilteredSeriesData() {
|
||||
return filteredSeriesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a series by key
|
||||
* @param {string} key - Series key
|
||||
* @returns {Object|undefined} Series object or undefined
|
||||
*/
|
||||
function findByKey(key) {
|
||||
return seriesData.find(function(s) {
|
||||
return s.key === key;
|
||||
});
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init: init,
|
||||
loadSeries: loadSeries,
|
||||
renderSeries: renderSeries,
|
||||
applyFiltersAndSort: applyFiltersAndSort,
|
||||
getSeriesData: getSeriesData,
|
||||
getFilteredSeriesData: getFilteredSeriesData,
|
||||
findByKey: findByKey
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user