Add frontend UI for async series loading

- Add SERIES_LOADING_UPDATE WebSocket event
- Update series cards to display loading indicators
- Add real-time status updates via WebSocket
- Include progress tracking (episodes, NFO, logo, images)
- Add CSS styling for loading states
- Implement updateSeriesLoadingStatus function
This commit is contained in:
2026-01-19 07:20:29 +01:00
parent f18c31a035
commit 0b4fb10d65
5 changed files with 249 additions and 5 deletions

View File

@@ -82,7 +82,12 @@ AniWorld.SeriesManager = (function() {
nfo_created_at: anime.nfo_created_at || null,
nfo_updated_at: anime.nfo_updated_at || null,
tmdb_id: anime.tmdb_id || null,
tvdb_id: anime.tvdb_id || null
tvdb_id: anime.tvdb_id || null,
loading_status: anime.loading_status || 'completed',
episodes_loaded: anime.episodes_loaded !== false,
nfo_loaded: anime.nfo_loaded !== false,
logo_loaded: anime.logo_loaded !== false,
images_loaded: anime.images_loaded !== false
};
});
} else if (data.status === 'success') {
@@ -274,6 +279,54 @@ AniWorld.SeriesManager = (function() {
});
}
/**
* Get loading indicator HTML for a series
* @param {Object} serie - Series data object
* @returns {string} HTML string
*/
function getLoadingIndicatorHTML(serie) {
const statusMessages = {
'pending': 'Queued for loading...',
'loading_episodes': 'Loading episodes...',
'loading_nfo': 'Generating NFO metadata...',
'loading_logo': 'Downloading logo...',
'loading_images': 'Downloading images...'
};
const statusMessage = statusMessages[serie.loading_status] || 'Loading...';
return '<div class="loading-indicator">' +
'<div class="loading-status">' +
'<span class="status-text"><i class="fas fa-spinner fa-spin"></i> ' + statusMessage + '</span>' +
'<div class="progress-items">' +
getProgressItemsHTML(serie) +
'</div>' +
'</div>' +
'</div>';
}
/**
* Get progress items HTML
* @param {Object} serie - Series data object
* @returns {string} HTML string
*/
function getProgressItemsHTML(serie) {
const items = [
{ key: 'episodes_loaded', label: 'Episodes' },
{ key: 'nfo_loaded', label: 'NFO' },
{ key: 'logo_loaded', label: 'Logo' },
{ key: 'images_loaded', label: 'Images' }
];
return items.map(function(item) {
const isLoaded = serie[item.key];
return '<div class="progress-item ' + (isLoaded ? 'completed' : 'pending') + '">' +
'<span class="icon">' + (isLoaded ? '✓' : '⋯') + '</span>' +
'<span class="label">' + item.label + '</span>' +
'</div>';
}).join('');
}
/**
* Create HTML for a series card
* @param {Object} serie - Series data object
@@ -284,10 +337,12 @@ AniWorld.SeriesManager = (function() {
const hasMissingEpisodes = serie.missing_episodes > 0;
const canBeSelected = hasMissingEpisodes;
const hasNfo = serie.has_nfo || false;
const isLoading = serie.loading_status && serie.loading_status !== 'completed' && serie.loading_status !== 'failed';
return '<div class="series-card ' + (isSelected ? 'selected' : '') + ' ' +
(hasMissingEpisodes ? 'has-missing' : 'complete') + '" ' +
'data-key="' + serie.key + '" data-folder="' + serie.folder + '">' +
(hasMissingEpisodes ? 'has-missing' : 'complete') + ' ' +
(isLoading ? 'loading' : '') + '" ' +
'data-key="' + serie.key + '" data-series-id="' + 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') + '>' +
@@ -308,6 +363,7 @@ AniWorld.SeriesManager = (function() {
'</div>' +
'<span class="series-site">' + serie.site + '</span>' +
'</div>' +
(isLoading ? getLoadingIndicatorHTML(serie) : '') +
'<div class="series-actions">' +
(hasNfo ?
'<button class="btn btn-sm btn-secondary nfo-view-btn" data-key="' + serie.key + '" title="View NFO">' +
@@ -347,6 +403,58 @@ AniWorld.SeriesManager = (function() {
});
}
/**
* Update series loading status from WebSocket event
* @param {Object} data - Loading status data
*/
function updateSeriesLoadingStatus(data) {
const seriesKey = data.series_id || data.series_key;
if (!seriesKey) return;
// Find series in data
const serie = findByKey(seriesKey);
if (!serie) return;
// Update series data
serie.loading_status = data.status || data.loading_status;
if (data.progress) {
serie.episodes_loaded = data.progress.episodes || false;
serie.nfo_loaded = data.progress.nfo || false;
serie.logo_loaded = data.progress.logo || false;
serie.images_loaded = data.progress.images || false;
}
// Update the specific card in the DOM
const seriesCard = document.querySelector('[data-series-id="' + seriesKey + '"]');
if (seriesCard) {
// If loading completed, reload to get fresh data
if (data.status === 'completed') {
loadSeries();
} else {
// Update loading indicator
const loadingIndicator = seriesCard.querySelector('.loading-indicator');
if (loadingIndicator) {
loadingIndicator.innerHTML = '<div class="loading-status">' +
'<span class="status-text"><i class="fas fa-spinner fa-spin"></i> ' +
(data.message || 'Loading...') + '</span>' +
'<div class="progress-items">' +
getProgressItemsHTML(serie) +
'</div>' +
'</div>';
} else if (serie.loading_status !== 'completed' && serie.loading_status !== 'failed') {
// Add loading indicator if it doesn't exist
const statsDiv = seriesCard.querySelector('.series-stats');
if (statsDiv) {
statsDiv.insertAdjacentHTML('afterend', getLoadingIndicatorHTML(serie));
}
}
// Add loading class to card
seriesCard.classList.add('loading');
}
}
}
// Public API
return {
init: init,
@@ -355,6 +463,7 @@ AniWorld.SeriesManager = (function() {
applyFiltersAndSort: applyFiltersAndSort,
getSeriesData: getSeriesData,
getFilteredSeriesData: getFilteredSeriesData,
findByKey: findByKey
findByKey: findByKey,
updateSeriesLoadingStatus: updateSeriesLoadingStatus
};
})();

View File

@@ -133,6 +133,14 @@ AniWorld.IndexSocketHandler = (function() {
AniWorld.ScanManager.updateProcessStatus('download', false, true);
});
// Series loading events
socket.on(WS_EVENTS.SERIES_LOADING_UPDATE, function(data) {
console.log('Series loading update:', data);
if (AniWorld.SeriesManager && AniWorld.SeriesManager.updateSeriesLoadingStatus) {
AniWorld.SeriesManager.updateSeriesLoadingStatus(data);
}
});
// Download events
socket.on(WS_EVENTS.DOWNLOAD_STARTED, function(data) {
isDownloading = true;

View File

@@ -26,6 +26,7 @@ AniWorld.Constants = (function() {
ANIME_RESCAN: '/api/anime/rescan',
ANIME_STATUS: '/api/anime/status',
ANIME_SCAN_STATUS: '/api/anime/scan/status',
ANIME_LOADING_STATUS: '/api/anime', // + /{key}/loading-status
// Queue endpoints
QUEUE_STATUS: '/api/queue/status',
@@ -99,6 +100,9 @@ AniWorld.Constants = (function() {
SCAN_ERROR: 'scan_error',
SCAN_FAILED: 'scan_failed',
// Series loading events
SERIES_LOADING_UPDATE: 'series_loading_update',
// Scheduled scan events
SCHEDULED_RESCAN_STARTED: 'scheduled_rescan_started',
SCHEDULED_RESCAN_COMPLETED: 'scheduled_rescan_completed',