648 lines
20 KiB
JavaScript
648 lines
20 KiB
JavaScript
// 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 = '<div style="background: var(--color-bg-elevated); padding: 1.5rem; border-radius: 0.5rem; border: 1px solid var(--color-border);">';
|
|
|
|
if (data.total === 0) {
|
|
html += '<p style="color: var(--color-text-secondary);">No results found</p>';
|
|
} else {
|
|
html += `<p style="color: var(--color-text-secondary); margin-bottom: 1rem;">Found ${data.total} results</p>`;
|
|
|
|
if (data.performers && data.performers.length > 0) {
|
|
html += '<h4 style="color: var(--color-brand); margin-top: 0;">Performers</h4><ul class="item-list">';
|
|
data.performers.slice(0, 5).forEach(p => {
|
|
html += `<li><a href="/performers/${p.id}">${p.name}</a></li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
if (data.studios && data.studios.length > 0) {
|
|
html += '<h4 style="color: var(--color-brand);">Studios</h4><ul class="item-list">';
|
|
data.studios.slice(0, 5).forEach(s => {
|
|
html += `<li><a href="/studios/${s.id}">${s.name}</a></li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
if (data.scenes && data.scenes.length > 0) {
|
|
html += '<h4 style="color: var(--color-brand);">Scenes</h4><ul class="item-list">';
|
|
data.scenes.slice(0, 5).forEach(sc => {
|
|
html += `<li><a href="/scenes/${sc.id}">${sc.title}</a></li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
|
|
if (data.tags && data.tags.length > 0) {
|
|
html += '<h4 style="color: var(--color-brand);">Tags</h4><div class="tag-list">';
|
|
data.tags.slice(0, 10).forEach(t => {
|
|
html += `<span class="tag">${t.name}</span>`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
}
|
|
|
|
html += '</div>';
|
|
resultsDiv.innerHTML = html;
|
|
resultsDiv.style.display = 'block';
|
|
}
|
|
|
|
// 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);
|
|
|
|
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);
|
|
} finally {
|
|
hideLoader();
|
|
}
|
|
}
|
|
|
|
async function bulkImportPerformers() {
|
|
showLoader('Importing performers from TPDB...');
|
|
if (!confirm('This will import ALL performers from TPDB. Continue?')) {
|
|
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?')) {
|
|
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?')) {
|
|
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() {
|
|
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 <url>\n' +
|
|
' ./goondex adultemp search-scene "Title"\n' +
|
|
' ./goondex adultemp scrape-scene <url>');
|
|
}
|
|
|
|
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';
|
|
}
|
|
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 = `
|
|
<div id="progress-modal" class="modal active">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Import Progress</h3>
|
|
</div>
|
|
<div id="progress-container">
|
|
<div class="progress-bar-wrapper">
|
|
<div class="progress-bar" id="progress-bar">
|
|
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
</div>
|
|
<div class="progress-text" id="progress-text">Starting...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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)';
|
|
}
|
|
}
|
|
}
|