Add MP4 scan progress visibility in UI

- Add broadcast_scan_started, broadcast_scan_progress, broadcast_scan_completed to WebSocketService
- Inject WebSocketService into AnimeService for real-time scan progress broadcasts
- Add CSS styles for scan progress overlay with spinner, stats, and completion state
- Update app.js to handle scan events and display progress overlay
- Add unit tests for new WebSocket broadcast methods
- All 1022 tests passing
This commit is contained in:
2025-12-23 18:24:32 +01:00
parent 9b071fe370
commit a24f07a36e
8 changed files with 633 additions and 20 deletions

View File

@@ -1898,4 +1898,190 @@ body {
.backup-actions .btn {
padding: 4px 8px;
font-size: 0.8em;
}
/* ========================================
Scan Progress Overlay Styles
======================================== */
.scan-progress-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.scan-progress-overlay.visible {
opacity: 1;
visibility: visible;
}
.scan-progress-container {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-elevated);
padding: var(--spacing-xxl);
max-width: 450px;
width: 90%;
text-align: center;
animation: scanProgressSlideIn 0.3s ease;
}
@keyframes scanProgressSlideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.scan-progress-header {
margin-bottom: var(--spacing-lg);
}
.scan-progress-header h3 {
margin: 0;
font-size: var(--font-size-title);
color: var(--color-text-primary);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
}
.scan-progress-spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid var(--color-bg-tertiary);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: scanSpinner 1s linear infinite;
}
@keyframes scanSpinner {
to {
transform: rotate(360deg);
}
}
.scan-progress-stats {
display: flex;
justify-content: space-around;
margin: var(--spacing-lg) 0;
padding: var(--spacing-md) 0;
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.scan-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
}
.scan-stat-value {
font-size: var(--font-size-large-title);
font-weight: 600;
color: var(--color-accent);
line-height: 1;
}
.scan-stat-label {
font-size: var(--font-size-caption);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.scan-current-directory {
margin-top: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius-md);
font-size: var(--font-size-caption);
color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.scan-current-directory-label {
font-weight: 500;
color: var(--color-text-tertiary);
margin-right: var(--spacing-xs);
}
/* Scan completed state */
.scan-progress-container.completed .scan-progress-spinner {
display: none;
}
.scan-progress-container.completed .scan-progress-header h3 {
color: var(--color-success);
}
.scan-completed-icon {
display: none;
width: 24px;
height: 24px;
color: var(--color-success);
}
.scan-progress-container.completed .scan-completed-icon {
display: inline-block;
}
.scan-progress-container.completed .scan-stat-value {
color: var(--color-success);
}
.scan-elapsed-time {
margin-top: var(--spacing-md);
font-size: var(--font-size-body);
color: var(--color-text-secondary);
}
.scan-elapsed-time i {
margin-right: var(--spacing-xs);
color: var(--color-text-tertiary);
}
/* Responsive adjustments for scan overlay */
@media (max-width: 768px) {
.scan-progress-container {
padding: var(--spacing-lg);
max-width: 95%;
}
.scan-progress-stats {
flex-direction: column;
gap: var(--spacing-md);
}
.scan-stat {
flex-direction: row;
justify-content: space-between;
width: 100%;
padding: 0 var(--spacing-md);
}
.scan-stat-value {
font-size: var(--font-size-title);
}
}

View File

@@ -202,19 +202,22 @@ class AniWorldApp {
this.updateConnectionStatus();
});
// Scan events
this.socket.on('scan_started', () => {
this.showStatus('Scanning series...', true);
// Scan events - handle new detailed scan progress overlay
this.socket.on('scan_started', (data) => {
console.log('Scan started:', data);
this.showScanProgressOverlay(data);
this.updateProcessStatus('rescan', true);
});
this.socket.on('scan_progress', (data) => {
this.updateStatus(`Scanning: ${data.folder} (${data.counter})`);
console.log('Scan progress:', data);
this.updateScanProgressOverlay(data);
});
// Handle both 'scan_completed' (legacy) and 'scan_complete' (new backend)
const handleScanComplete = () => {
this.hideStatus();
const handleScanComplete = (data) => {
console.log('Scan completed:', data);
this.hideScanProgressOverlay(data);
this.showToast('Scan completed successfully', 'success');
this.updateProcessStatus('rescan', false);
this.loadSeries();
@@ -1074,6 +1077,157 @@ class AniWorldApp {
document.getElementById('status-panel').classList.add('hidden');
}
/**
* Show the scan progress overlay with spinner and initial state
* @param {Object} data - Scan started event data
*/
showScanProgressOverlay(data) {
// Remove existing overlay if present
this.removeScanProgressOverlay();
// Create overlay element
const overlay = document.createElement('div');
overlay.id = 'scan-progress-overlay';
overlay.className = 'scan-progress-overlay';
overlay.innerHTML = `
<div class="scan-progress-container">
<div class="scan-progress-header">
<h3>
<span class="scan-progress-spinner"></span>
<i class="fas fa-check-circle scan-completed-icon"></i>
<span class="scan-title-text">Scanning Library</span>
</h3>
</div>
<div class="scan-progress-stats">
<div class="scan-stat">
<span class="scan-stat-value" id="scan-directories-count">0</span>
<span class="scan-stat-label">Directories</span>
</div>
<div class="scan-stat">
<span class="scan-stat-value" id="scan-files-count">0</span>
<span class="scan-stat-label">Series Found</span>
</div>
</div>
<div class="scan-current-directory" id="scan-current-directory">
<span class="scan-current-directory-label">Scanning:</span>
<span id="scan-current-path">${this.escapeHtml(data?.directory || 'Initializing...')}</span>
</div>
<div class="scan-elapsed-time hidden" id="scan-elapsed-time">
<i class="fas fa-clock"></i>
<span id="scan-elapsed-value">0.0s</span>
</div>
</div>
`;
document.body.appendChild(overlay);
// Trigger animation by adding visible class after a brief delay
requestAnimationFrame(() => {
overlay.classList.add('visible');
});
}
/**
* Update the scan progress overlay with current progress
* @param {Object} data - Scan progress event data
*/
updateScanProgressOverlay(data) {
const overlay = document.getElementById('scan-progress-overlay');
if (!overlay) return;
// Update directories count
const dirCount = document.getElementById('scan-directories-count');
if (dirCount && data.directories_scanned !== undefined) {
dirCount.textContent = data.directories_scanned;
}
// Update files/series count
const filesCount = document.getElementById('scan-files-count');
if (filesCount && data.files_found !== undefined) {
filesCount.textContent = data.files_found;
}
// Update current directory (truncate if too long)
const currentPath = document.getElementById('scan-current-path');
if (currentPath && data.current_directory) {
const maxLength = 50;
let displayPath = data.current_directory;
if (displayPath.length > maxLength) {
displayPath = '...' + displayPath.slice(-maxLength + 3);
}
currentPath.textContent = displayPath;
currentPath.title = data.current_directory; // Full path on hover
}
}
/**
* Hide the scan progress overlay with completion summary
* @param {Object} data - Scan completed event data
*/
hideScanProgressOverlay(data) {
const overlay = document.getElementById('scan-progress-overlay');
if (!overlay) return;
const container = overlay.querySelector('.scan-progress-container');
if (container) {
container.classList.add('completed');
}
// Update title
const titleText = overlay.querySelector('.scan-title-text');
if (titleText) {
titleText.textContent = 'Scan Complete';
}
// Update final stats
if (data) {
const dirCount = document.getElementById('scan-directories-count');
if (dirCount && data.total_directories !== undefined) {
dirCount.textContent = data.total_directories;
}
const filesCount = document.getElementById('scan-files-count');
if (filesCount && data.total_files !== undefined) {
filesCount.textContent = data.total_files;
}
// Show elapsed time
const elapsedTimeEl = document.getElementById('scan-elapsed-time');
const elapsedValueEl = document.getElementById('scan-elapsed-value');
if (elapsedTimeEl && elapsedValueEl && data.elapsed_seconds !== undefined) {
elapsedValueEl.textContent = `${data.elapsed_seconds.toFixed(1)}s`;
elapsedTimeEl.classList.remove('hidden');
}
// Update current directory to show completion message
const currentPath = document.getElementById('scan-current-path');
if (currentPath) {
currentPath.textContent = 'Scan finished successfully';
}
}
// Auto-dismiss after 3 seconds
setTimeout(() => {
this.removeScanProgressOverlay();
}, 3000);
}
/**
* Remove the scan progress overlay from the DOM
*/
removeScanProgressOverlay() {
const overlay = document.getElementById('scan-progress-overlay');
if (overlay) {
overlay.classList.remove('visible');
// Wait for fade out animation before removing
setTimeout(() => {
if (overlay.parentElement) {
overlay.remove();
}
}, 300);
}
}
showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}