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:
@@ -103,6 +103,77 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Series Card Loading State */
|
||||
.series-card.loading {
|
||||
border-color: var(--color-info);
|
||||
background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Loading Indicator */
|
||||
.loading-indicator {
|
||||
background: var(--color-surface-secondary, var(--color-surface));
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.loading-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.status-text i.fa-spinner {
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.progress-items {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
.progress-item.completed {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.progress-item.completed .icon {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.progress-item.pending {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.progress-item.pending .icon {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.progress-item .icon {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.progress-item .label {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Series Card Actions */
|
||||
.series-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user