Add common.loading locale key and cancellable capture test overlay

Add missing common.loading i18n key to en/ru locales. Add close button
and ESC key support to the overlay spinner so users can cancel running
capture tests. Uses AbortController to abort the in-flight fetch request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 17:20:09 +03:00
parent fb1086b309
commit c79b7367da
5 changed files with 58 additions and 3 deletions

View File

@@ -163,10 +163,28 @@ export function showOverlaySpinner(text, duration = 0) {
existing.remove();
}
// AbortController for cancelling in-flight fetch
window._overlayAbortController = new AbortController();
const overlay = document.createElement('div');
overlay.id = 'overlay-spinner';
overlay.className = 'overlay-spinner';
// Close button
const closeBtn = document.createElement('button');
closeBtn.className = 'overlay-spinner-close';
closeBtn.innerHTML = '&times;';
closeBtn.title = 'Cancel';
closeBtn.onclick = () => hideOverlaySpinner();
overlay.appendChild(closeBtn);
// ESC key handler
function onEsc(e) {
if (e.key === 'Escape') hideOverlaySpinner();
}
window._overlayEscHandler = onEsc;
document.addEventListener('keydown', onEsc);
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
@@ -235,6 +253,14 @@ export function hideOverlaySpinner() {
clearInterval(window.overlaySpinnerTimer);
window.overlaySpinnerTimer = null;
}
if (window._overlayEscHandler) {
document.removeEventListener('keydown', window._overlayEscHandler);
window._overlayEscHandler = null;
}
if (window._overlayAbortController) {
window._overlayAbortController.abort();
window._overlayAbortController = null;
}
const overlay = document.getElementById('overlay-spinner');
if (overlay) overlay.remove();
}

View File

@@ -298,6 +298,7 @@ export async function runTemplateTest() {
const template = window.currentTestingTemplate;
showOverlaySpinner(t('templates.test.running'), captureDuration);
const signal = window._overlayAbortController?.signal;
try {
const response = await fetchWithAuth('/capture-templates/test', {
@@ -307,7 +308,8 @@ export async function runTemplateTest() {
engine_config: template.engine_config,
display_index: parseInt(displayIndex),
capture_duration: captureDuration
})
}),
signal
});
if (!response.ok) {
@@ -319,6 +321,7 @@ export async function runTemplateTest() {
localStorage.setItem('lastTestDisplayIndex', displayIndex);
displayTestResults(result);
} catch (error) {
if (error.name === 'AbortError') return;
console.error('Error running test:', error);
hideOverlaySpinner();
showToast(t('templates.test.error.failed'), 'error');
@@ -980,11 +983,13 @@ export async function runStreamTest() {
if (!_currentTestStreamId) return;
const captureDuration = parseFloat(document.getElementById('test-stream-duration').value);
showOverlaySpinner(t('streams.test.running'), captureDuration);
const signal = window._overlayAbortController?.signal;
try {
const response = await fetchWithAuth(`/picture-sources/${_currentTestStreamId}/test`, {
method: 'POST',
body: JSON.stringify({ capture_duration: captureDuration })
body: JSON.stringify({ capture_duration: captureDuration }),
signal
});
if (!response.ok) {
const error = await response.json();
@@ -995,6 +1000,7 @@ export async function runStreamTest() {
const fullImageSrc = result.full_capture.full_image || result.full_capture.image;
openLightbox(fullImageSrc, buildTestStatsHtml(result));
} catch (error) {
if (error.name === 'AbortError') return;
console.error('Error running stream test:', error);
hideOverlaySpinner();
showToast(t('streams.test.error.failed') + ': ' + error.message, 'error');
@@ -1057,11 +1063,13 @@ export async function runPPTemplateTest() {
const captureDuration = parseFloat(document.getElementById('test-pp-duration').value);
showOverlaySpinner(t('postprocessing.test.running'), captureDuration);
const signal = window._overlayAbortController?.signal;
try {
const response = await fetchWithAuth(`/postprocessing-templates/${_currentTestPPTemplateId}/test`, {
method: 'POST',
body: JSON.stringify({ source_stream_id: sourceStreamId, capture_duration: captureDuration })
body: JSON.stringify({ source_stream_id: sourceStreamId, capture_duration: captureDuration }),
signal
});
if (!response.ok) {
const error = await response.json();
@@ -1072,6 +1080,7 @@ export async function runPPTemplateTest() {
const fullImageSrc = result.full_capture.full_image || result.full_capture.image;
openLightbox(fullImageSrc, buildTestStatsHtml(result));
} catch (error) {
if (error.name === 'AbortError') return;
console.error('Error running PP template test:', error);
hideOverlaySpinner();
showToast(t('postprocessing.test.error.failed') + ': ' + error.message, 'error');

View File

@@ -237,6 +237,7 @@
"confirm.title": "Confirm Action",
"confirm.yes": "Yes",
"confirm.no": "No",
"common.loading": "Loading...",
"common.delete": "Delete",
"common.edit": "Edit",
"streams.title": "\uD83D\uDCFA Sources",

View File

@@ -237,6 +237,7 @@
"confirm.title": "Подтверждение Действия",
"confirm.yes": "Да",
"confirm.no": "Нет",
"common.loading": "Загрузка...",
"common.delete": "Удалить",
"common.edit": "Редактировать",
"streams.title": "\uD83D\uDCFA Источники",

View File

@@ -1006,6 +1006,24 @@ input:-webkit-autofill:focus {
font-weight: 500;
}
.overlay-spinner-close {
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
font-size: 32px;
cursor: pointer;
line-height: 1;
padding: 4px 8px;
transition: color 0.15s;
}
.overlay-spinner-close:hover {
color: white;
}
@keyframes spin {
to { transform: rotate(360deg); }
}