Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_gallery_card.js
T

165 lines
8.1 KiB
JavaScript

/**
* One output image's card in the gallery: the picture (opens the viewer, or becomes a
* model's cover while one is being chosen; drags onto the canvas), Parameters (the image
* workbench), the star (ui_keep_menu.js: keep it as a workflow, a combo or its prompts;
* filled once kept as anything) and Delete (to the Recycle Bin, after a confirmation).
*/
import { translate as t } from './locales.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import { showImageWorkbench } from './ui_gallery_detail.js';
import { openKeepMenu } from './ui_keep_menu.js';
import { keptKey } from './image_keep.js';
const DETAILS_ICON = `<svg class="anomalous-gallery-details-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><line x1="2" y1="5" x2="8" y2="5"></line><line x1="12" y1="5" x2="14" y2="5"></line><circle cx="10" cy="5" r="2"></circle><line x1="2" y1="11" x2="4" y2="11"></line><line x1="8" y1="11" x2="14" y2="11"></line><circle cx="6" cy="11" r="2"></circle></svg>`;
const DELETE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
function button(className, title, onClick) {
const node = document.createElement('button');
node.type = 'button';
node.className = className;
node.title = title;
node.onclick = (event) => {
event.stopPropagation();
onClick(event);
};
return node;
}
const sameItem = (item, image) => item.filename === image.filename && item.subfolder === image.subfolder;
/** Uses the image as the cover of the model being given one (the gallery's cover mode). */
async function useAsCover(owner, image) {
const model = owner.gallerySelectModel;
try {
const response = await fetch('/anomalous/set_custom_cover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: owner.currentType,
path_idx: owner.currentPathIdx,
subfolder: owner.currentSubfolder,
filename: model.filename,
source_image: image.subfolder ? `${image.subfolder}/${image.filename}` : image.filename,
}),
});
const data = await response.json();
if (data.status !== 'success') throw new Error(data.message || 'cover not set');
} catch (error) {
await anomalousAlert(t('galleryErrorPrefix') + error.message);
return;
}
owner.gallerySelectModel = null;
const banner = document.getElementById('anomalous-gallery-select-banner');
if (banner) banner.style.display = 'none';
owner.galleryPanel.classList.remove('is-cover-selecting');
owner.galleryPanel.style.display = 'none';
await owner.loadModels();
const updated = owner.models.find(m => m.filename === model.filename);
if (owner.currentDetailModel && owner.currentDetailModel.filename === model.filename) {
owner.detailPanel.style.display = 'flex';
if (updated) owner.showDetail(updated);
} else {
owner.grid.style.display = 'grid';
}
}
function openWorkbench(owner, image, url) {
const list = owner.galleryImagesList || [];
const index = list.findIndex(item => sameItem(item, image));
void showImageWorkbench(owner, image, url, {
items: list,
currentIndex: Math.max(0, index),
loadMore: async () => {
if (owner.galleryHasMore && !owner.galleryLoading) await owner.loadGalleryImages(owner.galleryCurrentPage + 1);
return owner.galleryImagesList || [];
},
});
}
async function deleteImage(owner, image, card) {
if (!await anomalousConfirm(t('galleryDeleteConfirmHint'), t('galleryDeleteConfirm'), { okLabel: t('galleryDelete') })) return;
try {
const response = await fetch('/anomalous/delete_gallery_image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: image.filename, subfolder: image.subfolder }),
});
const data = await response.json();
if (data.status !== 'success') throw new Error(data.message);
card.remove();
owner.galleryImagesList = (owner.galleryImagesList || []).filter(item => !sameItem(item, image));
} catch (error) {
await anomalousAlert(t('galleryDeleteFailed') + (error?.message || ''));
}
}
/** The star: ☆, or ★ once the image is kept as anything. */
function keepButton(owner, image) {
const star = button('anomalous-gallery-keep', t('galleryKeep'), () => openKeepMenu(owner, star, image, {
onKept: (kept) => {
mark(kept);
// Cards drawn later (another page) read the same list.
owner.galleryKept?.then(map => map.set(keptKey(image), { ...kept })).catch(() => {});
},
}));
const mark = (kept) => {
const done = Boolean(kept?.recipe || kept?.combo || kept?.prompt);
star.textContent = done ? '★' : '☆';
star.classList.toggle('is-kept', done);
star.title = t(done ? 'galleryKept' : 'galleryKeep');
};
mark(null);
owner.galleryKept?.then(map => mark(map.get(keptKey(image)))).catch(() => {});
return star;
}
/** The card of `imgData` ({ filename, subfolder } from GET /anomalous/gallery_images). */
export function createGalleryCard(owner, imgData, { showViewer }) {
const image = { type: 'output', filename: imgData.filename, subfolder: imgData.subfolder || '' };
const query = `filename=${encodeURIComponent(image.filename)}&subfolder=${encodeURIComponent(image.subfolder)}`;
const url = `/view?${query}&type=output`;
const card = document.createElement('div');
card.className = 'anomalous-gallery-card';
const isVideo = /\.(mp4|webm|mov|avi|mkv)$/i.test(image.filename);
if (isVideo) card.classList.add('is-video');
const img = document.createElement('img');
img.src = `/anomalous/output_thumbnail?${query}`; // drags and the viewer use the original
img.loading = 'lazy';
img.title = isVideo ? t('galleryViewVideo') : t('materialViewOriginal');
// The card is dragged, not the <img>: Chrome adds a dragged <img> as a file, and ComfyUI
// would load that thumbnail (no workflow in it) instead of fetching the original's link.
img.draggable = false;
card.draggable = true;
card.addEventListener('dragstart', (event) => {
const fullUrl = new URL(url, window.location.href).href;
event.dataTransfer.setData('text/uri-list', fullUrl);
event.dataTransfer.setData('text/plain', fullUrl);
// Chromium may not start dragging very large (hires) images without a small drag image.
if (window.anomalousDragGhostImg) event.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
});
img.onclick = () => (owner.gallerySelectModel ? useAsCover(owner, image) : showViewer(url, isVideo));
const details = button('anomalous-gallery-details', t('materialViewDetails'), () => {
if (!owner.gallerySelectModel) openWorkbench(owner, image, url);
});
details.innerHTML = DETAILS_ICON; // static markup only
details.append(Object.assign(document.createElement('span'), { textContent: t('materialViewParameters') }));
const remove = button('anomalous-gallery-delete', t('galleryDelete'), () => deleteImage(owner, image, card));
remove.innerHTML = DELETE_ICON; // static markup only
card.append(img, details, remove);
if (isVideo) {
const ext = image.filename.split('.').pop().toUpperCase();
const badge = document.createElement('span');
badge.className = 'anomalous-gallery-video-badge';
badge.innerHTML = `<svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 3"/></svg> ${ext}`;
card.append(badge);
}
// Only PNG outputs carry the workflow that can be kept.
if (/\.png$/i.test(image.filename)) card.append(keepButton(owner, image));
return card;
}