refactor: split CSS and JS into modular files (SRP)

This commit is contained in:
2025-12-26 13:55:02 +01:00
parent 94cf36bff3
commit 2e5731b5d6
47 changed files with 8882 additions and 2298 deletions

View 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
};
})();