491 lines
19 KiB
JavaScript

// Image management functionality
let currentPage = 1;
let totalPages = 1;
let imageBlobCache = new Map(); // Cache for blob URLs
// Load image with authentication and return blob URL
async function loadImageBlob(imageId) {
// Check cache first
if (imageBlobCache.has(imageId)) {
return imageBlobCache.get(imageId);
}
try {
const blobUrl = await apiClient.downloadImageBlob(imageId);
imageBlobCache.set(imageId, blobUrl);
return blobUrl;
} catch (error) {
console.error(`Failed to load image ${imageId}:`, error);
throw error; // Let the caller handle the error
}
}
// Clean up blob URLs to prevent memory leaks
function cleanupBlobCache() {
for (const [imageId, blobUrl] of imageBlobCache.entries()) {
if (blobUrl.startsWith('blob:')) {
URL.revokeObjectURL(blobUrl);
}
}
imageBlobCache.clear();
}
// Load images with pagination
async function loadImages(page = 1, tags = null) {
if (!config.isConfigured()) {
showAlert('Please configure your API settings first.', 'warning');
return;
}
const container = document.getElementById('imagesContainer');
container.innerHTML = '<div class="text-center"><div class="loading-spinner"></div> Loading images...</div>';
try {
const response = await apiClient.getImages(page, 20, tags);
currentPage = page;
totalPages = Math.ceil(response.total / (response.limit || 20));
// Handle structured response - extract images array from response object
const images = response.images || response;
displayImages(images);
displayPagination(response);
} catch (error) {
handleApiError(error, 'loading images');
container.innerHTML = '<div class="alert alert-danger">Failed to load images</div>';
}
}
// Display images in grid
async function displayImages(images) {
const container = document.getElementById('imagesContainer');
if (!images || images.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-images fa-3x text-muted mb-3"></i>
<h4>No images found</h4>
<p class="text-muted">Upload your first image to get started!</p>
<button class="btn btn-primary" onclick="showUploadModal()">
<i class="fas fa-plus me-1"></i>Upload Image
</button>
</div>
`;
return;
}
// Create image cards with placeholder images first
const imagesHtml = images.map(image => `
<div class="image-card card">
<div class="image-container" style="position: relative; height: 200px; background-color: #f8f9fa; display: flex; align-items: center; justify-content: center;">
<img id="img-${image.id}"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InRyYW5zcGFyZW50Ii8+PC9zdmc+"
alt="${escapeHtml(image.description || 'Image')}"
onclick="viewImage('${image.id}')"
style="cursor: pointer; opacity: 0; width: 100%; height: 200px; object-fit: cover; position: absolute; top: 0; left: 0;">
<div class="loading-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2;">
<div class="loading-spinner large"></div>
<div class="mt-2 text-muted small">Loading...</div>
</div>
</div>
<div class="card-body">
<h6 class="card-title">${escapeHtml(truncateText(image.description || 'Untitled', 50))}</h6>
<p class="card-text small text-muted">
<i class="fas fa-calendar me-1"></i>${formatDate(image.upload_date)}
</p>
${image.tags && image.tags.length > 0 ? `
<div class="mb-2">
${image.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
<div class="btn-group w-100" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="viewImage('${image.id}')">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="editImage('${image.id}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteImage('${image.id}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
container.innerHTML = `<div class="image-grid">${imagesHtml}</div>`;
// Load actual images asynchronously
for (const image of images) {
loadImageBlob(image.id).then(blobUrl => {
const imgElement = document.getElementById(`img-${image.id}`);
const loadingOverlay = imgElement.parentElement.querySelector('.loading-overlay');
if (imgElement) {
imgElement.src = blobUrl;
imgElement.style.opacity = '1';
if (loadingOverlay) {
loadingOverlay.style.display = 'none';
}
}
}).catch(error => {
console.error(`Failed to load image ${image.id}:`, error);
const imgElement = document.getElementById(`img-${image.id}`);
const loadingOverlay = imgElement.parentElement.querySelector('.loading-overlay');
if (imgElement && loadingOverlay) {
// Show error state instead of broken image
loadingOverlay.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-exclamation-triangle fa-2x mb-2"></i>
<div class="small">Failed to load</div>
</div>
`;
imgElement.style.display = 'none'; // Hide the img element completely
}
});
}
}
// Display pagination
function displayPagination(response) {
const container = document.getElementById('imagesContainer');
if (totalPages <= 1) return;
const paginationHtml = `
<nav class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadImages(${currentPage - 1})">Previous</a>
</li>
${Array.from({length: Math.min(totalPages, 5)}, (_, i) => {
const page = i + 1;
return `
<li class="page-item ${page === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="loadImages(${page})">${page}</a>
</li>
`;
}).join('')}
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="loadImages(${currentPage + 1})">Next</a>
</li>
</ul>
</nav>
`;
container.insertAdjacentHTML('beforeend', paginationHtml);
}
// Show upload modal
function showUploadModal() {
const modalBody = `
<form id="uploadForm">
<div class="mb-3">
<label class="form-label">Select Image</label>
<div class="upload-area" id="uploadArea">
<i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i>
<h5>Drag & drop an image here</h5>
<p class="text-muted">or click to select a file</p>
<input type="file" id="imageFile" accept="image/*" style="display: none;">
</div>
<div id="imagePreview" class="mt-3" style="display: none;">
<img id="previewImg" class="img-fluid rounded" style="max-height: 200px;">
</div>
</div>
<div class="mb-3">
<label for="imageDescription" class="form-label">Description</label>
<textarea class="form-control" id="imageDescription" rows="3"
placeholder="Describe this image..."></textarea>
</div>
<div class="mb-3">
<label for="imageTags" class="form-label">Tags</label>
<input type="text" class="form-control" id="imageTags"
placeholder="Enter tags separated by commas">
<div class="form-text">e.g., nature, landscape, sunset</div>
</div>
</form>
`;
const modalFooter = `
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="uploadImage()">
<i class="fas fa-upload me-1"></i>Upload
</button>
`;
const modal = createModal('uploadModal', 'Upload Image', modalBody, modalFooter);
// Setup file input and drag & drop
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('imageFile');
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelection(e.target.files[0]);
}
});
setupFileDropZone(uploadArea, (files) => {
if (files.length > 0) {
handleFileSelection(files[0]);
}
});
modal.show();
}
// Handle file selection
async function handleFileSelection(file) {
try {
validateFile(file);
const preview = await createImagePreview(file);
const previewContainer = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
previewImg.src = preview;
previewContainer.style.display = 'block';
// Store file for upload
document.getElementById('uploadForm').selectedFile = file;
} catch (error) {
showAlert(error.message, 'danger');
}
}
// Upload image
async function uploadImage() {
const form = document.getElementById('uploadForm');
const file = form.selectedFile;
if (!file) {
showAlert('Please select an image file', 'danger');
return;
}
const description = document.getElementById('imageDescription').value.trim();
const tagsInput = document.getElementById('imageTags').value.trim();
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
const uploadButton = document.querySelector('#uploadModal .btn-primary');
setLoadingState(uploadButton);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('description', description);
if (tags.length > 0) {
formData.append('tags', tags.join(','));
}
await apiClient.uploadImage(formData);
showAlert('Image uploaded successfully!', 'success');
// Close modal and refresh images
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
removeModal('uploadModal');
loadImages(currentPage);
} catch (error) {
handleApiError(error, 'uploading image');
} finally {
setLoadingState(uploadButton, false);
}
}
// View image details
async function viewImage(imageId) {
try {
// Close any existing modals first
const existingModals = ['viewImageModal', 'editImageModal'];
existingModals.forEach(modalId => {
const existingModal = document.getElementById(modalId);
if (existingModal) {
const bsModal = bootstrap.Modal.getInstance(existingModal);
if (bsModal) {
bsModal.hide();
}
removeModal(modalId);
}
});
const image = await apiClient.getImage(imageId);
// Load the image with authentication
const imageSrc = await loadImageBlob(imageId);
// Use unique modal ID to prevent conflicts
const modalId = `viewImageModal-${imageId}`;
const modalBody = `
<div class="text-center mb-3">
<img src="${imageSrc}" class="img-fluid rounded"
style="max-height: 400px;" alt="${escapeHtml(image.description || 'Image')}">
</div>
<div class="row">
<div class="col-md-6">
<h6>Description</h6>
<p>${escapeHtml(image.description || 'No description')}</p>
</div>
<div class="col-md-6">
<h6>Details</h6>
<p><strong>Created:</strong> ${formatDate(image.upload_date)}</p>
<p><strong>Size:</strong> ${formatFileSize(image.file_size)}</p>
<p><strong>Type:</strong> ${image.content_type}</p>
</div>
</div>
${image.tags && image.tags.length > 0 ? `
<div class="mt-3">
<h6>Tags</h6>
${image.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
`;
const modalFooter = `
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="editImage('${imageId}')">
<i class="fas fa-edit me-1"></i>Edit
</button>
<button type="button" class="btn btn-danger" onclick="deleteImage('${imageId}')">
<i class="fas fa-trash me-1"></i>Delete
</button>
`;
const modal = createModal(modalId, 'Image Details', modalBody, modalFooter);
// Add cleanup when modal is hidden
const modalElement = document.getElementById(modalId);
modalElement.addEventListener('hidden.bs.modal', () => {
removeModal(modalId);
});
modal.show();
} catch (error) {
handleApiError(error, 'loading image details');
}
}
// Edit image
async function editImage(imageId) {
try {
// Close any existing modals first
const existingModals = document.querySelectorAll('.modal');
existingModals.forEach(modal => {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) {
bsModal.hide();
}
modal.remove();
});
const image = await apiClient.getImage(imageId);
// Load the image with authentication
const imageSrc = await loadImageBlob(imageId);
// Use unique modal ID to prevent conflicts
const modalId = `editImageModal-${imageId}`;
const modalBody = `
<form id="editImageForm-${imageId}">
<div class="mb-3 text-center">
<img src="${imageSrc}" class="img-fluid rounded"
style="max-height: 200px;" alt="${escapeHtml(image.description || 'Image')}">
</div>
<div class="mb-3">
<label for="editDescription-${imageId}" class="form-label">Description</label>
<textarea class="form-control" id="editDescription-${imageId}" rows="3">${escapeHtml(image.description || '')}</textarea>
</div>
<div class="mb-3">
<label for="editTags-${imageId}" class="form-label">Tags</label>
<input type="text" class="form-control" id="editTags-${imageId}"
value="${image.tags ? image.tags.join(', ') : ''}">
<div class="form-text">Enter tags separated by commas</div>
</div>
</form>
`;
const modalFooter = `
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveImageChanges('${imageId}')">
<i class="fas fa-save me-1"></i>Save Changes
</button>
`;
const modal = createModal(modalId, 'Edit Image', modalBody, modalFooter);
// Add cleanup when modal is hidden
const modalElement = document.getElementById(modalId);
modalElement.addEventListener('hidden.bs.modal', () => {
removeModal(modalId);
});
modal.show();
} catch (error) {
handleApiError(error, 'loading image for editing');
}
}
// Save image changes
async function saveImageChanges(imageId) {
const description = document.getElementById(`editDescription-${imageId}`).value.trim();
const tagsInput = document.getElementById(`editTags-${imageId}`).value.trim();
const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
const saveButton = document.querySelector(`#editImageModal-${imageId} .btn-primary`);
setLoadingState(saveButton);
try {
await apiClient.updateImage(imageId, {
description,
tags
});
showAlert('Image updated successfully!', 'success');
// Close modal and refresh images
bootstrap.Modal.getInstance(document.getElementById(`editImageModal-${imageId}`)).hide();
removeModal(`editImageModal-${imageId}`);
loadImages(currentPage);
} catch (error) {
handleApiError(error, 'updating image');
} finally {
setLoadingState(saveButton, false);
}
}
// Delete image
function deleteImage(imageId) {
confirmAction('Are you sure you want to delete this image? This action cannot be undone.', async () => {
try {
await apiClient.deleteImage(imageId);
showAlert('Image deleted successfully!', 'success');
loadImages(currentPage);
// Close any open modals for this image
const modalsToClose = [`viewImageModal-${imageId}`, `editImageModal-${imageId}`];
modalsToClose.forEach(modalId => {
const modalElement = document.getElementById(modalId);
if (modalElement) {
const bsModal = bootstrap.Modal.getInstance(modalElement);
if (bsModal) {
bsModal.hide();
}
removeModal(modalId);
}
});
} catch (error) {
handleApiError(error, 'deleting image');
}
});
}