// Goondex Web UI JavaScript
// Modal handling
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
}
}
// Import functions
// Global Search
let searchTimeout;
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('global-search');
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
document.getElementById('global-search-results').style.display = 'none';
return;
}
searchTimeout = setTimeout(() => globalSearch(query), 300);
});
}
});
async function globalSearch(query) {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const result = await response.json();
if (result.success) {
displayGlobalSearchResults(result.data);
}
} catch (error) {
console.error('Search failed:', error);
}
}
function displayGlobalSearchResults(data) {
const resultsDiv = document.getElementById('global-search-results');
let html = '
';
if (data.total === 0) {
html += '
No results found
';
} else {
html += `
Found ${data.total} results
`;
if (data.performers && data.performers.length > 0) {
html += '
Performers
';
data.performers.slice(0, 5).forEach(p => {
html += `- ${p.name}
`;
});
html += '
';
}
if (data.studios && data.studios.length > 0) {
html += '
Studios
';
data.studios.slice(0, 5).forEach(s => {
html += `- ${s.name}
`;
});
html += '
';
}
if (data.scenes && data.scenes.length > 0) {
html += '
Scenes
';
data.scenes.slice(0, 5).forEach(sc => {
html += `- ${sc.title}
`;
});
html += '
';
}
if (data.tags && data.tags.length > 0) {
html += '
Tags
';
data.tags.slice(0, 10).forEach(t => {
html += `${t.name}`;
});
html += '
';
}
}
html += '
';
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
}
// Bulk Import Functions
async function bulkImportAll() {
showLoader('Importing everything from TPDB...');
startJobProgress('TPDB bulk import');
try {
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...');
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();
}
}
async function bulkImportStudios() {
showLoader('Importing studios from TPDB...');
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();
}
}
async function bulkImportScenes() {
showLoader('Importing scenes from TPDB...');
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();
}
}
function bulkImportMovies() {
alert('Bulk movie import is not implemented yet. Use the Adult Empire CLI to scrape movies individually for now.');
}
function toggleFilterbar() {
document.getElementById('filterbar').classList.toggle('open');
}
function showTPDBDisabled() {
alert('TPDB import/sync is temporarily disabled. Use Adult Empire CLI commands instead:\n\n' +
' ./goondex adultemp search-performer "Name"\n' +
' ./goondex adultemp scrape-performer \n' +
' ./goondex adultemp search-scene "Title"\n' +
' ./goondex adultemp scrape-scene ');
}
function scrollToAEImport() {
const section = document.getElementById('ae-import');
if (section) {
section.scrollIntoView({ behavior: 'smooth' });
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard:\n' + text);
}).catch(() => {
alert('Could not copy. Please copy manually:\n' + text);
});
}
// ============================================================================
// Adult Empire UI helpers
// ============================================================================
function setAEStatus(msg, isError = false) {
const el = document.getElementById('ae-status');
if (!el) return;
el.textContent = msg;
el.classList.toggle('error', !!isError);
el.style.display = msg ? 'block' : 'none';
}
async function aeImportPerformerByName() {
const name = prompt('Import performer by name (Adult Empire):');
if (!name) return;
setAEStatus(`Searching Adult Empire for "${name}"...`);
showLoader(`Importing performer "${name}" from Adult Empire...`);
try {
const res = await fetch('/api/ae/import/performer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: name })
});
const result = await res.json();
if (result.success) {
setAEStatus(result.message);
setTimeout(() => location.reload(), 1500);
} else {
setAEStatus(result.message || 'Import failed', true);
}
} catch (err) {
setAEStatus(`Error: ${err.message}`, true);
} finally {
hideLoader();
}
}
async function aeImportPerformerByURL() {
const url = prompt('Paste Adult Empire performer URL:');
if (!url) return;
setAEStatus('Importing performer from Adult Empire URL...');
showLoader('Importing performer from Adult Empire URL...');
try {
const res = await fetch('/api/ae/import/performer-by-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const result = await res.json();
if (result.success) {
setAEStatus(result.message);
setTimeout(() => location.reload(), 1500);
} else {
setAEStatus(result.message || 'Import failed', true);
}
} catch (err) {
setAEStatus(`Error: ${err.message}`, true);
} finally {
hideLoader();
}
}
async function aeImportSceneByName() {
const title = prompt('Import scene by title (Adult Empire):');
if (!title) return;
setAEStatus(`Searching Adult Empire for "${title}"...`);
showLoader(`Importing scene "${title}" from Adult Empire...`);
try {
const res = await fetch('/api/ae/import/scene', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: title })
});
const result = await res.json();
if (result.success) {
setAEStatus(result.message);
setTimeout(() => location.reload(), 1500);
} else {
setAEStatus(result.message || 'Import failed', true);
}
} catch (err) {
setAEStatus(`Error: ${err.message}`, true);
} finally {
hideLoader();
}
}
async function aeImportSceneByURL() {
const url = prompt('Paste Adult Empire scene URL:');
if (!url) return;
setAEStatus('Importing scene from Adult Empire URL...');
showLoader('Importing scene from Adult Empire URL...');
try {
const res = await fetch('/api/ae/import/scene-by-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const result = await res.json();
if (result.success) {
setAEStatus(result.message);
setTimeout(() => location.reload(), 1500);
} else {
setAEStatus(result.message || 'Import failed', true);
}
} catch (err) {
setAEStatus(`Error: ${err.message}`, true);
} finally {
hideLoader();
}
}
function applyFilters() {
// Hook for your search/filter logic
console.log("Applying filters…");
}
function sortTable(columnIndex) {
const table = document.querySelector(".gx-table tbody");
const rows = Array.from(table.querySelectorAll("tr"));
const asc = table.getAttribute("data-sort") !== "asc";
table.setAttribute("data-sort", asc ? "asc" : "desc");
rows.sort((a, b) => {
const A = a.children[columnIndex].innerText.trim().toLowerCase();
const B = b.children[columnIndex].innerText.trim().toLowerCase();
if (!isNaN(A) && !isNaN(B)) return asc ? A - B : B - A;
return asc ? A.localeCompare(B) : B.localeCompare(A);
});
rows.forEach(r => table.appendChild(r));
}
// Search-based Import Functions
async function importPerformer() {
const query = document.getElementById('performer-query').value;
if (!query) {
alert('Please enter a performer name');
return;
}
setImportStatus('performer', 'Searching...', false);
try {
const response = await fetch('/api/import/performer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('performer', result.message, true);
setTimeout(() => {
closeModal('search-performer-modal');
location.reload();
}, 1500);
} else {
setImportStatus('performer', result.message, false);
}
} catch (error) {
setImportStatus('performer', 'Error: ' + error.message, false);
}
}
async function importStudio() {
const query = document.getElementById('studio-query').value;
if (!query) {
alert('Please enter a studio name');
return;
}
setImportStatus('studio', 'Searching...', false);
try {
const response = await fetch('/api/import/studio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('studio', result.message, true);
setTimeout(() => {
closeModal('search-studio-modal');
location.reload();
}, 1500);
} else {
setImportStatus('studio', result.message, false);
}
} catch (error) {
setImportStatus('studio', 'Error: ' + error.message, false);
}
}
async function importScene() {
const query = document.getElementById('scene-query').value;
if (!query) {
alert('Please enter a scene title');
return;
}
setImportStatus('scene', 'Searching...', false);
try {
const response = await fetch('/api/import/scene', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const result = await response.json();
if (result.success) {
setImportStatus('scene', result.message, true);
setTimeout(() => {
closeModal('search-scene-modal');
location.reload();
}, 1500);
} else {
setImportStatus('scene', result.message, false);
}
} catch (error) {
setImportStatus('scene', 'Error: ' + error.message, false);
}
}
async function syncAll() {
const forceEl = document.getElementById('sync-force');
const force = forceEl ? forceEl.checked : false;
setImportStatus('sync', 'Syncing all data from TPDB...', false);
try {
const response = await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force })
});
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.Updated} updated, ${r.Failed} failed\n`;
});
}
setImportStatus('sync', message, true);
setTimeout(() => {
closeModal('sync-modal');
location.reload();
}, 2000);
} else {
setImportStatus('sync', result.message, false);
}
} catch (error) {
setImportStatus('sync', 'Error: ' + error.message, false);
}
}
function setImportStatus(type, message, success) {
const statusEl = document.getElementById(`${type}-import-status`);
if (statusEl) {
statusEl.textContent = message;
statusEl.className = 'result-message ' + (success ? 'success' : 'error');
statusEl.style.display = 'block';
}
}
// Close modals when clicking outside
// Global loader helpers
function showLoader(msg) {
const overlay = document.getElementById('global-loader');
const text = document.getElementById('global-loader-text');
if (overlay) {
overlay.style.display = 'flex';
}
if (text && msg) {
text.textContent = msg;
}
}
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');
}
}
// Image error handling
function handleImageError(img) {
img.style.display = 'none';
}
// Progress Modal Functions
function showProgressModal(entityType) {
const modal = document.getElementById('progress-modal');
if (!modal) {
// Create progress modal if it doesn't exist
const modalHTML = `
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
} else {
modal.classList.add('active');
}
}
function closeProgressModal() {
const modal = document.getElementById('progress-modal');
if (modal) {
modal.classList.remove('active');
}
}
function updateProgress(entityType, current, total, message, isError) {
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
if (progressFill && progressText) {
const percent = total > 0 ? (current / total * 100) : 0;
progressFill.style.width = percent + '%';
progressText.textContent = message;
if (isError) {
progressFill.style.background = 'var(--color-warning)';
progressText.style.color = 'var(--color-warning)';
} else {
progressFill.style.background = 'linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%)';
progressText.style.color = 'var(--color-text-primary)';
}
}
}