Add inline TPDB import progress bar and remove import confirmations

This commit is contained in:
Stu Leak 2025-12-04 12:38:10 -05:00
parent 4067f9b793
commit 919cf65f30
3 changed files with 157 additions and 136 deletions

View File

@ -578,6 +578,50 @@ main.container {
to { transform: rotate(360deg); }
}
.job-progress {
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%);
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 0.75rem 1rem;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
z-index: 1900;
min-width: 320px;
max-width: 520px;
}
.job-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: var(--color-text-primary);
}
.job-progress-bar {
margin-top: 0.6rem;
height: 10px;
background: var(--color-bg-elevated);
border-radius: 999px;
overflow: hidden;
}
.job-progress-fill {
height: 100%;
background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%);
width: 0%;
transition: width 0.2s ease;
}
.job-progress-message {
margin-top: 0.4rem;
font-size: 0.9rem;
color: var(--color-text-secondary);
}
/* Detail views */
.breadcrumb {
margin-bottom: 1.5rem;

View File

@ -97,164 +97,62 @@ function displayGlobalSearchResults(data) {
// Bulk Import Functions
async function bulkImportAll() {
showLoader('Importing all data from TPDB...');
if (!confirm('This will import ALL data from TPDB. This may take several hours. Continue?')) {
hideLoader();
return;
}
setImportStatus('import-all', 'Importing all data from TPDB... This may take a while.', false);
showLoader('Importing everything from TPDB...');
startJobProgress('TPDB bulk import');
try {
const response = await fetch('/api/import/all', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
let message = result.message + '\n\n';
if (result.data) {
result.data.forEach(r => {
message += `${r.EntityType}: ${r.Imported}/${r.Total} imported, ${r.Failed} failed\n`;
});
}
setImportStatus('import-all', message, true);
setTimeout(() => {
closeModal('import-all-modal');
location.reload();
}, 3000);
} else {
setImportStatus('import-all', result.message, false);
}
} catch (error) {
setImportStatus('import-all', 'Error: ' + error.message, false);
await importWithProgress('/api/import/all-performers/progress', 'Performers');
await importWithProgress('/api/import/all-studios/progress', 'Studios');
await importWithProgress('/api/import/all-scenes/progress', 'Scenes');
setImportStatus('import-all', 'Bulk import complete', true);
setTimeout(() => location.reload(), 1500);
} catch (err) {
setImportStatus('import-all', `Bulk import error: ${err.message}`, false);
} finally {
stopJobProgress();
hideLoader();
}
}
async function bulkImportPerformers() {
showLoader('Importing performers from TPDB...');
if (!confirm('This will import ALL performers from TPDB. Continue?')) {
startJobProgress('Importing performers from TPDB');
try {
await importWithProgress('/api/import/all-performers/progress', 'Performers');
setTimeout(() => location.reload(), 1000);
} catch (err) {
setImportStatus('performer', `Error: ${err.message}`, false);
} finally {
stopJobProgress();
hideLoader();
return;
}
// Show progress modal
showProgressModal('performers');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-performers/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('performers', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('performers', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} performers`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('performers', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('performers', 0, 0, 'Connection error', true);
eventSource.close();
hideLoader();
};
}
async function bulkImportStudios() {
showLoader('Importing studios from TPDB...');
if (!confirm('This will import ALL studios from TPDB. Continue?')) {
startJobProgress('Importing studios from TPDB');
try {
await importWithProgress('/api/import/all-studios/progress', 'Studios');
setTimeout(() => location.reload(), 1000);
} catch (err) {
setImportStatus('studio', `Error: ${err.message}`, false);
} finally {
stopJobProgress();
hideLoader();
return;
}
// Show progress modal
showProgressModal('studios');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-studios/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('studios', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('studios', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} studios`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('studios', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('studios', 0, 0, 'Connection error', true);
eventSource.close();
hideLoader();
};
}
async function bulkImportScenes() {
showLoader('Importing scenes from TPDB...');
if (!confirm('This will import ALL scenes from TPDB. Continue?')) {
startJobProgress('Importing scenes from TPDB');
try {
await importWithProgress('/api/import/all-scenes/progress', 'Scenes');
setTimeout(() => location.reload(), 1000);
} catch (err) {
setImportStatus('scene', `Error: ${err.message}`, false);
} finally {
stopJobProgress();
hideLoader();
return;
}
// Show progress modal
showProgressModal('scenes');
// Connect to SSE endpoint
const eventSource = new EventSource('/api/import/all-scenes/progress');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateProgress('scenes', 0, 0, data.error, true);
eventSource.close();
return;
}
if (data.complete) {
updateProgress('scenes', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} scenes`, false);
eventSource.close();
setTimeout(() => {
closeProgressModal();
location.reload();
}, 2000);
} else {
updateProgress('scenes', data.current, data.total, data.message, false);
}
};
eventSource.onerror = function() {
updateProgress('scenes', 0, 0, 'Connection error', true);
eventSource.close();
hideLoader();
};
}
function bulkImportMovies() {
@ -581,6 +479,75 @@ function hideLoader() {
const overlay = document.getElementById('global-loader');
if (overlay) overlay.style.display = 'none';
}
// Unified SSE import helper with progress bar
function importWithProgress(url, label) {
return new Promise((resolve, reject) => {
const eventSource = new EventSource(url);
startJobProgress(label);
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.error) {
updateJobProgress(0, 0, data.error, true);
eventSource.close();
reject(new Error(data.error));
return;
}
if (data.complete) {
updateJobProgress(data.result.Imported, data.result.Total, `${label} complete (${data.result.Imported}/${data.result.Total})`, false);
eventSource.close();
resolve(data.result);
return;
}
updateJobProgress(data.current, data.total, data.message, false);
};
eventSource.onerror = function() {
updateJobProgress(0, 0, `${label} connection error`, true);
eventSource.close();
reject(new Error('Connection error'));
};
});
}
function startJobProgress(label) {
const container = document.getElementById('job-progress');
const lbl = document.getElementById('job-progress-label');
const msg = document.getElementById('job-progress-message');
const fill = document.getElementById('job-progress-fill');
const count = document.getElementById('job-progress-count');
if (container && lbl && msg && fill && count) {
container.style.display = 'block';
lbl.textContent = label || 'Working...';
msg.textContent = '';
count.textContent = '';
fill.style.width = '0%';
}
}
function updateJobProgress(current, total, message, isError) {
const container = document.getElementById('job-progress');
const msg = document.getElementById('job-progress-message');
const fill = document.getElementById('job-progress-fill');
const count = document.getElementById('job-progress-count');
if (container && fill && msg && count) {
const percent = total > 0 ? Math.min(100, (current / total) * 100) : 0;
fill.style.width = `${percent}%`;
count.textContent = total > 0 ? `${current}/${total}` : '';
msg.textContent = message || '';
if (isError) {
fill.style.background = '#ff8a8a';
msg.style.color = '#ff8a8a';
} else {
fill.style.background = 'linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%)';
msg.style.color = 'var(--color-text-secondary)';
}
}
}
function stopJobProgress() {
const container = document.getElementById('job-progress');
if (container) container.style.display = 'none';
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('active');

View File

@ -63,4 +63,14 @@
<div id="global-loader-text">Working...</div>
</div>
</div>
<div id="job-progress" class="job-progress" style="display:none;">
<div class="job-progress-header">
<span id="job-progress-label">Importing...</span>
<span id="job-progress-count"></span>
</div>
<div class="job-progress-bar">
<div class="job-progress-fill" id="job-progress-fill" style="width:0%"></div>
</div>
<div class="job-progress-message" id="job-progress-message"></div>
</div>
{{end}}