491 lines
19 KiB
JavaScript
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');
|
|
}
|
|
});
|
|
}
|