// 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() {
if (!confirm('This will import ALL data from TPDB. This may take several hours. Continue?')) {
return;
}
setImportStatus('import-all', 'Importing all data from TPDB... This may take a while.', false);
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);
}
}
async function bulkImportPerformers() {
if (!confirm('This will import ALL performers from TPDB. Continue?')) {
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();
};
}
async function bulkImportStudios() {
if (!confirm('This will import ALL studios from TPDB. Continue?')) {
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();
};
}
async function bulkImportScenes() {
if (!confirm('This will import ALL scenes from TPDB. Continue?')) {
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();
};
}
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}"...`);
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);
}
}
async function aeImportPerformerByURL() {
const url = prompt('Paste Adult Empire performer URL:');
if (!url) return;
setAEStatus('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);
}
}
async function aeImportSceneByName() {
const title = prompt('Import scene by title (Adult Empire):');
if (!title) return;
setAEStatus(`Searching Adult Empire for "${title}"...`);
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);
}
}
async function aeImportSceneByURL() {
const url = prompt('Paste Adult Empire scene URL:');
if (!url) return;
setAEStatus('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);
}
}
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
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)';
}
}
}