mirror of
https://github.com/yusufipk/imagen-openrouter.git
synced 2026-09-11 10:36:07 +00:00
fix: update button types for better accessibility and enhance incremental gallery updates
This commit is contained in:
+4
-4
@@ -32,7 +32,7 @@
|
|||||||
<div class="reference-slots" id="referenceSlots">
|
<div class="reference-slots" id="referenceSlots">
|
||||||
<!-- Slots rendered dynamically by JS -->
|
<!-- Slots rendered dynamically by JS -->
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-ghost" id="clearReferences">Clear All References</button>
|
<button type="button" class="btn btn-ghost" id="clearReferences">Clear All References</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Selection -->
|
<!-- Model Selection -->
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>OpenRouter API Key</h3>
|
<h3>OpenRouter API Key</h3>
|
||||||
<input type="password" id="apiKey" placeholder="sk-or-..." class="text-input">
|
<input type="password" id="apiKey" placeholder="sk-or-..." class="text-input">
|
||||||
<button class="btn btn-ghost" id="saveApiKey">Save Key</button>
|
<button type="button" class="btn btn-ghost" id="saveApiKey">Save Key</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
rows="3"></textarea>
|
rows="3"></textarea>
|
||||||
<div class="prompt-actions">
|
<div class="prompt-actions">
|
||||||
<span class="char-count" id="charCount">0 chars</span>
|
<span class="char-count" id="charCount">0 chars</span>
|
||||||
<button class="btn btn-primary" id="generateBtn">
|
<button type="button" class="btn btn-primary" id="generateBtn">
|
||||||
Generate
|
Generate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<div class="gallery-header">
|
<div class="gallery-header">
|
||||||
<h2>Generated Images</h2>
|
<h2>Generated Images</h2>
|
||||||
<div class="gallery-actions">
|
<div class="gallery-actions">
|
||||||
<button class="btn btn-ghost" id="clearGallery">Clear Gallery</button>
|
<button type="button" class="btn btn-ghost" id="clearGallery">Clear Gallery</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery" id="gallery">
|
<div class="gallery" id="gallery">
|
||||||
|
|||||||
+193
-6
@@ -626,7 +626,9 @@ async function generateImages() {
|
|||||||
failed: 0
|
failed: 0
|
||||||
};
|
};
|
||||||
state.pendingBatches.push(batch);
|
state.pendingBatches.push(batch);
|
||||||
renderGallery(); // Show loading placeholders
|
|
||||||
|
// Add loading placeholders without full re-render
|
||||||
|
addLoadingPlaceholders(batch, imageCount);
|
||||||
|
|
||||||
showToast(`Queued ${imageCount} image(s) for generation`, 'success');
|
showToast(`Queued ${imageCount} image(s) for generation`, 'success');
|
||||||
|
|
||||||
@@ -649,18 +651,21 @@ async function generateImages() {
|
|||||||
};
|
};
|
||||||
state.images.unshift(imageData);
|
state.images.unshift(imageData);
|
||||||
batch.completed++;
|
batch.completed++;
|
||||||
renderGallery();
|
|
||||||
|
// Remove one placeholder and add the new image
|
||||||
|
removeOnePlaceholder(batchId);
|
||||||
|
prependImageCard(imageData, 0);
|
||||||
|
|
||||||
// Save to IndexedDB in background
|
// Save to IndexedDB in background
|
||||||
ImagenDB.saveImage(imageData).catch(e => console.error('Failed to save to IndexedDB:', e));
|
ImagenDB.saveImage(imageData).catch(e => console.error('Failed to save to IndexedDB:', e));
|
||||||
} else {
|
} else {
|
||||||
batch.failed++;
|
batch.failed++;
|
||||||
renderGallery();
|
removeOnePlaceholder(batchId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate image:', error);
|
console.error('Failed to generate image:', error);
|
||||||
batch.failed++;
|
batch.failed++;
|
||||||
renderGallery();
|
removeOnePlaceholder(batchId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -678,7 +683,6 @@ async function generateImages() {
|
|||||||
if (batchIndex !== -1) {
|
if (batchIndex !== -1) {
|
||||||
state.pendingBatches.splice(batchIndex, 1);
|
state.pendingBatches.splice(batchIndex, 1);
|
||||||
}
|
}
|
||||||
renderGallery();
|
|
||||||
|
|
||||||
if (batch.completed > 0) {
|
if (batch.completed > 0) {
|
||||||
showToast(`${batch.completed} image(s) generated!`, 'success');
|
showToast(`${batch.completed} image(s) generated!`, 'success');
|
||||||
@@ -953,6 +957,176 @@ function renderGallery() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Incremental Gallery Updates =====
|
||||||
|
function addLoadingPlaceholders(batch, count) {
|
||||||
|
// Hide empty state if showing
|
||||||
|
elements.galleryEmpty.style.display = 'none';
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const placeholder = createPlaceholderElement(batch);
|
||||||
|
elements.gallery.insertBefore(placeholder, elements.gallery.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPlaceholderElement(batch) {
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'image-card loading-placeholder';
|
||||||
|
placeholder.dataset.batchId = batch.id;
|
||||||
|
const truncatedPrompt = batch.prompt.length > 60 ? batch.prompt.substring(0, 60) + '...' : batch.prompt;
|
||||||
|
placeholder.innerHTML = `
|
||||||
|
<div class="loading-placeholder-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span class="loading-placeholder-text">Generating...</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-card-overlay" style="opacity: 1;">
|
||||||
|
<p class="image-card-prompt">${escapeHtml(truncatedPrompt)}</p>
|
||||||
|
<div class="image-card-meta">
|
||||||
|
<span class="meta-tag">${escapeHtml(batch.modelName)}</span>
|
||||||
|
<span class="meta-tag loading-tag">
|
||||||
|
<svg class="spin-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="2" x2="12" y2="6"></line>
|
||||||
|
<line x1="12" y1="18" x2="12" y2="22"></line>
|
||||||
|
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
|
||||||
|
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
|
||||||
|
<line x1="2" y1="12" x2="6" y2="12"></line>
|
||||||
|
<line x1="18" y1="12" x2="22" y2="12"></line>
|
||||||
|
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
|
||||||
|
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
|
||||||
|
</svg>
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeOnePlaceholder(batchId) {
|
||||||
|
const placeholder = elements.gallery.querySelector(`.loading-placeholder[data-batch-id="${batchId}"]`);
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show empty state if gallery is now empty
|
||||||
|
if (elements.gallery.children.length === 0 ||
|
||||||
|
(elements.gallery.children.length === 1 && elements.gallery.contains(elements.galleryEmpty))) {
|
||||||
|
elements.galleryEmpty.style.display = 'flex';
|
||||||
|
if (!elements.gallery.contains(elements.galleryEmpty)) {
|
||||||
|
elements.gallery.appendChild(elements.galleryEmpty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependImageCard(image, index) {
|
||||||
|
const card = createImageCardElement(image, index);
|
||||||
|
|
||||||
|
// Insert after any remaining placeholders
|
||||||
|
const firstNonPlaceholder = elements.gallery.querySelector('.image-card:not(.loading-placeholder)');
|
||||||
|
if (firstNonPlaceholder) {
|
||||||
|
elements.gallery.insertBefore(card, firstNonPlaceholder);
|
||||||
|
} else {
|
||||||
|
elements.gallery.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update indices on existing cards since we prepended
|
||||||
|
updateCardIndices();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImageCardElement(image, index) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'image-card';
|
||||||
|
card.dataset.imageId = image.id;
|
||||||
|
|
||||||
|
const safeUrl = sanitizeImageUrl(image.url);
|
||||||
|
const safePrompt = escapeHtml(image.prompt);
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="image-card-actions image-card-actions-top">
|
||||||
|
<button class="image-card-btn image-card-download" title="Download image">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="image-card-btn image-card-delete" title="Delete image">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="image-card-actions image-card-actions-bottom">
|
||||||
|
<button class="image-card-btn image-card-reference" title="Use as reference">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||||||
|
<circle cx="12" cy="10" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="image-card-btn image-card-recreate" title="Recreate with same settings">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
|
<polyline points="1 20 1 14 7 14"></polyline>
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<img src="${safeUrl}" alt="${safePrompt}" loading="lazy">
|
||||||
|
<div class="image-card-overlay">
|
||||||
|
<p class="image-card-prompt">${safePrompt}</p>
|
||||||
|
<div class="image-card-meta">
|
||||||
|
<span class="meta-tag">${escapeHtml(image.modelName || image.model)}</span>
|
||||||
|
<span class="meta-tag">${escapeHtml(image.quality || image.size)}</span>
|
||||||
|
<span class="meta-tag">${escapeHtml(image.aspectRatio)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Attach event handlers
|
||||||
|
attachImageCardHandlers(card, image);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachImageCardHandlers(card, image) {
|
||||||
|
const imageId = image.id;
|
||||||
|
|
||||||
|
card.querySelector('.image-card-download').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = state.images.findIndex(img => img.id === imageId);
|
||||||
|
if (idx !== -1) downloadImageByIndex(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.querySelector('.image-card-delete').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = state.images.findIndex(img => img.id === imageId);
|
||||||
|
if (idx !== -1) deleteImage(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.querySelector('.image-card-reference').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = state.images.findIndex(img => img.id === imageId);
|
||||||
|
if (idx !== -1) addImageAsReference(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.querySelector('.image-card-recreate').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = state.images.findIndex(img => img.id === imageId);
|
||||||
|
if (idx !== -1) recreateImageByIndex(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const idx = state.images.findIndex(img => img.id === imageId);
|
||||||
|
if (idx !== -1) openModal(state.images[idx]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCardIndices() {
|
||||||
|
// No longer needed since we use image IDs instead of indices
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteImage(index) {
|
async function deleteImage(index) {
|
||||||
const imageToDelete = state.images[index];
|
const imageToDelete = state.images[index];
|
||||||
state.images.splice(index, 1);
|
state.images.splice(index, 1);
|
||||||
@@ -963,7 +1137,20 @@ async function deleteImage(index) {
|
|||||||
console.warn('Could not delete from IndexedDB:', e);
|
console.warn('Could not delete from IndexedDB:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGallery();
|
// Remove card from DOM without full re-render
|
||||||
|
const card = elements.gallery.querySelector(`.image-card[data-image-id="${imageToDelete.id}"]`);
|
||||||
|
if (card) {
|
||||||
|
card.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show empty state if gallery is now empty
|
||||||
|
if (state.images.length === 0 && state.pendingBatches.length === 0) {
|
||||||
|
elements.galleryEmpty.style.display = 'flex';
|
||||||
|
if (!elements.gallery.contains(elements.galleryEmpty)) {
|
||||||
|
elements.gallery.appendChild(elements.galleryEmpty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showToast('Image deleted', 'success');
|
showToast('Image deleted', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user