Add circular progress indicator with percentage for capture tests
Some checks failed
Validate / validate (push) Failing after 9s

- Replace simple spinner with SVG circular progress ring
- Show progress percentage (0-100%) in center of ring
- Animate progress based on capture duration
- Update every 100ms for smooth animation
- Clean up timer on completion or cancellation

UI improvements:
- Full-page overlay with blur backdrop
- Large percentage display (28px)
- Progress ring uses primary color
- Clean, minimal design

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 18:52:24 +03:00
parent 17bb4110ec
commit c371e07e81
2 changed files with 469 additions and 33 deletions

View File

@@ -2328,6 +2328,115 @@ function closeTemplateModal() {
currentEditingTemplateId = null;
}
// Show full-page overlay spinner with progress
function showOverlaySpinner(text, duration = 0) {
// Remove existing overlay if any
const existing = document.getElementById('overlay-spinner');
if (existing) {
// Clear any existing timer
if (window.overlaySpinnerTimer) {
clearInterval(window.overlaySpinnerTimer);
window.overlaySpinnerTimer = null;
}
existing.remove();
}
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'overlay-spinner';
overlay.className = 'overlay-spinner';
// Create progress container
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
// Create SVG progress ring
const radius = 56;
const circumference = 2 * Math.PI * radius;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '120');
svg.setAttribute('height', '120');
svg.setAttribute('class', 'progress-ring');
// Background circle
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bgCircle.setAttribute('class', 'progress-ring-bg');
bgCircle.setAttribute('cx', '60');
bgCircle.setAttribute('cy', '60');
bgCircle.setAttribute('r', radius);
// Progress circle
const progressCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
progressCircle.setAttribute('class', 'progress-ring-circle');
progressCircle.setAttribute('cx', '60');
progressCircle.setAttribute('cy', '60');
progressCircle.setAttribute('r', radius);
progressCircle.style.strokeDasharray = circumference;
progressCircle.style.strokeDashoffset = circumference;
svg.appendChild(bgCircle);
svg.appendChild(progressCircle);
// Create progress content (percentage display)
const progressContent = document.createElement('div');
progressContent.className = 'progress-content';
const progressPercentage = document.createElement('div');
progressPercentage.className = 'progress-percentage';
progressPercentage.textContent = '0%';
progressContent.appendChild(progressPercentage);
progressContainer.appendChild(svg);
progressContainer.appendChild(progressContent);
// Create text
const spinnerText = document.createElement('div');
spinnerText.className = 'spinner-text';
spinnerText.textContent = text;
overlay.appendChild(progressContainer);
overlay.appendChild(spinnerText);
document.body.appendChild(overlay);
// Animate progress if duration is provided
if (duration > 0) {
const startTime = Date.now();
window.overlaySpinnerTimer = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
const progress = Math.min(elapsed / duration, 1);
const percentage = Math.round(progress * 100);
// Update progress ring
const offset = circumference - (progress * circumference);
progressCircle.style.strokeDashoffset = offset;
// Update percentage display
progressPercentage.textContent = `${percentage}%`;
// Stop timer if complete
if (progress >= 1) {
clearInterval(window.overlaySpinnerTimer);
window.overlaySpinnerTimer = null;
}
}, 100);
}
}
// Hide full-page overlay spinner
function hideOverlaySpinner() {
// Clear timer if exists
if (window.overlaySpinnerTimer) {
clearInterval(window.overlaySpinnerTimer);
window.overlaySpinnerTimer = null;
}
const overlay = document.getElementById('overlay-spinner');
if (overlay) overlay.remove();
}
// Update capture duration and save to localStorage
function updateCaptureDuration(value) {
document.getElementById('test-template-duration-value').textContent = value;
@@ -2546,26 +2655,8 @@ async function runTemplateTest() {
const template = window.currentTestingTemplate;
const resultsDiv = document.getElementById('test-template-results');
// Show loading state without destroying the structure
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading';
loadingDiv.textContent = t('templates.test.running');
loadingDiv.style.position = 'absolute';
loadingDiv.style.inset = '0';
loadingDiv.style.background = 'var(--bg-primary)';
loadingDiv.style.display = 'flex';
loadingDiv.style.alignItems = 'center';
loadingDiv.style.justifyContent = 'center';
loadingDiv.style.zIndex = '10';
loadingDiv.id = 'test-loading-overlay';
// Remove old loading overlay if exists
const oldLoading = document.getElementById('test-loading-overlay');
if (oldLoading) oldLoading.remove();
resultsDiv.style.display = 'block';
resultsDiv.style.position = 'relative';
resultsDiv.appendChild(loadingDiv);
// Show full-page overlay spinner with progress
showOverlaySpinner(t('templates.test.running'), captureDuration);
try {
const response = await fetchWithAuth('/capture-templates/test', {
@@ -2587,9 +2678,8 @@ async function runTemplateTest() {
displayTestResults(result);
} catch (error) {
console.error('Error running test:', error);
// Remove loading overlay
const loadingOverlay = document.getElementById('test-loading-overlay');
if (loadingOverlay) loadingOverlay.remove();
// Hide overlay spinner
hideOverlaySpinner();
// Show short error in snack, details are in console
showToast(t('templates.test.error.failed'), 'error');
}
@@ -2599,9 +2689,8 @@ async function runTemplateTest() {
function displayTestResults(result) {
const resultsDiv = document.getElementById('test-template-results');
// Remove loading overlay
const loadingOverlay = document.getElementById('test-loading-overlay');
if (loadingOverlay) loadingOverlay.remove();
// Hide overlay spinner
hideOverlaySpinner();
// Full capture preview
const previewImg = document.getElementById('test-template-preview-image');