Pages made at different times used their own greys (slate here, neutral there) and accents (sky, violet, indigo), and most ignored the Abyssal Scarlet theme. About 1,850 literal colours in web/styles and in CSS and inline styles written from JS now use the --amb-* tokens by what they are for: surfaces, text (new --amb-text-soft and --amb-text-dim), borders (new --amb-border-strong), the primary button, and one accent (new --amb-link, tinted with color-mix). Both themes define the new tokens. Literal colours stay where the colour is the meaning: status, prompt roles, voice emotions, canvas node colours, shadows and overlays; theme override rules are untouched. The share-code dialogs move from inline styles to .anomalous-share-* classes. Stylesheet versions are bumped. Checked: text contrast on every rail page, Prompt Studio, Prompt Notes, Model Sources, settings, model detail and editor, share codes, in both themes; structure check and JS tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
725 lines
33 KiB
JavaScript
725 lines
33 KiB
JavaScript
/**
|
|
* ui_gallery.js
|
|
* Extracted Gallery Viewer methods.
|
|
*/
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { translate } from './locales.js';
|
|
import { showImageMaterialDetail } from './ui_materials.js';
|
|
import { createSearchChips } from './ui_search_chips.js';
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
export async function refreshGalleryImages() {
|
|
if (this.galleryLoading || this.galleryRefreshLoading) return;
|
|
this.galleryRefreshLoading = true;
|
|
try {
|
|
const scrollTop = this.galleryGrid?.scrollTop || 0;
|
|
await this.loadGalleryImages(1, true);
|
|
if (this.galleryGrid) this.galleryGrid.scrollTop = scrollTop;
|
|
} catch (error) {
|
|
console.warn('Could not refresh output gallery:', error);
|
|
} finally {
|
|
this.galleryRefreshLoading = false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Search blocks above the output gallery; the terms live on the browser as `gallerySearchTerms`. */
|
|
export function createGallerySearchBar(owner) {
|
|
const search = createSearchChips({
|
|
placeholder: t('gallerySearchPlaceholder'),
|
|
help: t('gallerySearchHelp'),
|
|
onChange: (terms) => {
|
|
owner.gallerySearchTerms = terms;
|
|
owner.loadGalleryImages(1, true, { refresh: false });
|
|
},
|
|
});
|
|
search.element.classList.add('anomalous-gallery-search');
|
|
owner.gallerySearchCount = search.countElement;
|
|
return search.element;
|
|
}
|
|
|
|
export async function loadGalleryImages(page = 1, reset = false, { refresh = reset } = {}) {
|
|
if (this.galleryLoading) {
|
|
// A newer query arrived while loading: run it once the current request settles.
|
|
if (reset) this.galleryReloadPending = true;
|
|
return;
|
|
}
|
|
this.galleryLoading = true;
|
|
const terms = [...(this.gallerySearchTerms || [])];
|
|
const termsKey = JSON.stringify(terms);
|
|
const query = terms.length > 0;
|
|
this.gallerySentinel.textContent = query && page === 1 ? t('gallerySearching') : t('galleryLoading');
|
|
|
|
try {
|
|
const params = new URLSearchParams({ page: String(page), limit: '50' });
|
|
if (refresh) params.set('refresh', '1');
|
|
terms.forEach(term => params.append('term', term));
|
|
const res = await fetch(`/anomalous/gallery_images?${params}`);
|
|
const data = await res.json();
|
|
if (termsKey !== JSON.stringify(this.gallerySearchTerms || [])) {
|
|
this.galleryReloadPending = true;
|
|
return;
|
|
}
|
|
if (this.gallerySearchCount) {
|
|
this.gallerySearchCount.textContent = query && Number.isFinite(data.total) ? t('gallerySearchCount', { count: data.total }) : '';
|
|
}
|
|
|
|
if (reset) {
|
|
// Clear existing cards
|
|
const cards = this.galleryGrid.querySelectorAll('.anomalous-gallery-card');
|
|
cards.forEach(c => c.remove());
|
|
this.galleryLoaded = true;
|
|
this.galleryImagesList = [];
|
|
}
|
|
|
|
if (data.images && data.images.length > 0) {
|
|
const incomingItems = data.images.map(imgData => {
|
|
const q_sub = encodeURIComponent(imgData.subfolder);
|
|
const q_file = encodeURIComponent(imgData.filename);
|
|
return {
|
|
filename: imgData.filename,
|
|
subfolder: imgData.subfolder || '',
|
|
url: `/view?filename=${q_file}&subfolder=${q_sub}&type=output`,
|
|
sourceImage: {
|
|
type: 'output',
|
|
filename: imgData.filename,
|
|
subfolder: imgData.subfolder || '',
|
|
}
|
|
};
|
|
});
|
|
if (reset) {
|
|
this.galleryImagesList = incomingItems;
|
|
} else {
|
|
this.galleryImagesList = [...(this.galleryImagesList || []), ...incomingItems];
|
|
}
|
|
|
|
data.images.forEach(imgData => {
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-gallery-card';
|
|
|
|
const q_sub = encodeURIComponent(imgData.subfolder);
|
|
const q_file = encodeURIComponent(imgData.filename);
|
|
const imgUrl = `/view?filename=${q_file}&subfolder=${q_sub}&type=output`;
|
|
|
|
const img = document.createElement('img');
|
|
img.src = `/anomalous/output_thumbnail?filename=${q_file}&subfolder=${q_sub}`; // drags and the viewer use the original
|
|
img.loading = 'lazy';
|
|
img.draggable = true;
|
|
img.title = t('materialViewOriginal');
|
|
|
|
// Drag and drop support for ComfyUI
|
|
img.addEventListener('dragstart', (e) => {
|
|
const fullUrl = new URL(imgUrl, window.location.href).href;
|
|
e.dataTransfer.setData('text/uri-list', fullUrl);
|
|
e.dataTransfer.setData('text/plain', fullUrl);
|
|
|
|
// Fix for Chromium failing to initiate drag for extremely large (Hires Fix) images
|
|
if (window.anomalousDragGhostImg) {
|
|
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
|
|
}
|
|
});
|
|
|
|
const openDetail = () => {
|
|
const curIdx = (this.galleryImagesList || []).findIndex(it => it.filename === imgData.filename && it.subfolder === (imgData.subfolder || ''));
|
|
void showImageMaterialDetail(this, {
|
|
type: 'output',
|
|
filename: imgData.filename,
|
|
subfolder: imgData.subfolder || '',
|
|
}, imgUrl, {
|
|
items: this.galleryImagesList || [],
|
|
currentIndex: curIdx >= 0 ? curIdx : 0,
|
|
loadMore: async () => {
|
|
if (this.galleryHasMore && !this.galleryLoading) {
|
|
await this.loadGalleryImages(this.galleryCurrentPage + 1);
|
|
}
|
|
return this.galleryImagesList || [];
|
|
}
|
|
});
|
|
};
|
|
|
|
// Click to view
|
|
img.onclick = () => {
|
|
if (this.gallerySelectModel) {
|
|
const model = this.gallerySelectModel;
|
|
fetch('/anomalous/set_custom_cover', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
source_image: imgData.subfolder ? imgData.subfolder + '/' + imgData.filename : imgData.filename
|
|
})
|
|
}).then(res => res.json()).then(async data => {
|
|
if (data.status === 'success') {
|
|
const tempModel = this.gallerySelectModel;
|
|
this.gallerySelectModel = null;
|
|
const banner = document.getElementById('anomalous-gallery-select-banner');
|
|
if (banner) banner.style.display = 'none';
|
|
this.galleryPanel.classList.remove('is-cover-selecting');
|
|
this.galleryPanel.style.display = 'none';
|
|
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
|
|
if (this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
this.detailPanel.style.display = 'flex';
|
|
if (updatedModel) this.showDetail(updatedModel);
|
|
} else {
|
|
this.grid.style.display = 'grid';
|
|
// Grid was already refreshed by loadModels
|
|
}
|
|
|
|
|
|
} else {
|
|
alert(t('galleryErrorPrefix') + data.message);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
showGalleryViewer(imgUrl);
|
|
};
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.className = 'anomalous-gallery-delete';
|
|
delBtn.innerHTML = '<svg style="width:14px;height:14px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
|
|
delBtn.title = t('galleryDelete');
|
|
|
|
delBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.style.position = 'absolute';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100%';
|
|
overlay.style.height = '100%';
|
|
overlay.style.background = 'rgba(0,0,0,0.85)';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.flexDirection = 'column';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.gap = '15px';
|
|
overlay.style.zIndex = '10';
|
|
|
|
const msg = document.createElement('div');
|
|
const msgTitle = document.createElement('div');
|
|
msgTitle.textContent = t('galleryDeleteConfirm');
|
|
const msgHint = document.createElement('div');
|
|
msgHint.textContent = t('galleryDeleteConfirmHint');
|
|
msgHint.style.fontSize = '0.8em';
|
|
msgHint.style.color = '#aaa';
|
|
msg.append(msgTitle, msgHint);
|
|
msg.style.color = '#fff';
|
|
msg.style.fontWeight = 'bold';
|
|
msg.style.textAlign = 'center';
|
|
|
|
const btnRow = document.createElement('div');
|
|
btnRow.style.display = 'flex';
|
|
btnRow.style.gap = '12px';
|
|
|
|
const confirmBtn = document.createElement('button');
|
|
confirmBtn.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>${t('galleryDelete')}`;
|
|
confirmBtn.style.background = '#dc3545';
|
|
confirmBtn.style.color = '#fff';
|
|
confirmBtn.style.border = 'none';
|
|
confirmBtn.style.padding = '10px 16px';
|
|
confirmBtn.style.borderRadius = '6px';
|
|
confirmBtn.style.cursor = 'pointer';
|
|
confirmBtn.style.fontWeight = 'bold';
|
|
confirmBtn.style.transition = 'background 0.2s';
|
|
confirmBtn.onmouseover = () => confirmBtn.style.background = '#ff0000';
|
|
confirmBtn.onmouseout = () => confirmBtn.style.background = '#dc3545';
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = t('galleryCancel');
|
|
cancelBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
cancelBtn.style.color = '#fff';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.padding = '10px 16px';
|
|
cancelBtn.style.borderRadius = '6px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.style.transition = 'background 0.2s';
|
|
cancelBtn.onmouseover = () => cancelBtn.style.background = '#666';
|
|
cancelBtn.onmouseout = () => cancelBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
|
|
cancelBtn.onclick = (ce) => {
|
|
ce.stopPropagation();
|
|
overlay.remove();
|
|
};
|
|
|
|
confirmBtn.onclick = async (ce) => {
|
|
ce.stopPropagation();
|
|
confirmBtn.textContent = t('galleryDeleting');
|
|
confirmBtn.disabled = true;
|
|
try {
|
|
const dr = await fetch('/anomalous/delete_gallery_image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename: imgData.filename, subfolder: imgData.subfolder })
|
|
});
|
|
const dd = await dr.json();
|
|
if (dd.status === 'success') {
|
|
card.remove();
|
|
if (this.galleryImagesList) {
|
|
this.galleryImagesList = this.galleryImagesList.filter(it => !(it.filename === imgData.filename && it.subfolder === (imgData.subfolder || '')));
|
|
}
|
|
} else {
|
|
alert(t('galleryDeleteFailed') + dd.message);
|
|
overlay.remove();
|
|
}
|
|
} catch (err) {
|
|
alert(t('galleryErrorPrefix') + err);
|
|
overlay.remove();
|
|
}
|
|
};
|
|
|
|
btnRow.appendChild(cancelBtn);
|
|
btnRow.appendChild(confirmBtn);
|
|
overlay.appendChild(msg);
|
|
overlay.appendChild(btnRow);
|
|
|
|
card.appendChild(overlay);
|
|
};
|
|
|
|
const detailsBtn = document.createElement('button');
|
|
detailsBtn.className = 'anomalous-gallery-details';
|
|
detailsBtn.type = 'button';
|
|
detailsBtn.innerHTML = `
|
|
<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>
|
|
<span>${t('materialViewParameters')}</span>
|
|
`;
|
|
detailsBtn.title = t('materialViewDetails');
|
|
detailsBtn.onclick = (event) => {
|
|
event.stopPropagation();
|
|
if (this.gallerySelectModel) return;
|
|
openDetail();
|
|
};
|
|
|
|
card.appendChild(img);
|
|
card.appendChild(detailsBtn);
|
|
card.appendChild(delBtn);
|
|
this.galleryGrid.insertBefore(card, this.gallerySentinel);
|
|
});
|
|
|
|
this.galleryCurrentPage = page;
|
|
this.galleryHasMore = page < data.pages;
|
|
|
|
if (!this.galleryHasMore) {
|
|
this.gallerySentinel.textContent = t('galleryNoMore');
|
|
} else {
|
|
this.gallerySentinel.textContent = t('galleryScrollMore');
|
|
}
|
|
} else {
|
|
this.galleryHasMore = false;
|
|
this.gallerySentinel.textContent = reset ? t(query ? 'gallerySearchEmpty' : 'galleryEmpty') : t('galleryNoMore');
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load gallery images', e);
|
|
this.gallerySentinel.textContent = t('galleryLoadFailed');
|
|
} finally {
|
|
this.galleryLoading = false;
|
|
if (this.galleryReloadPending) {
|
|
this.galleryReloadPending = false;
|
|
void this.loadGalleryImages(1, true, { refresh: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function showGeneratedGallery(model) {
|
|
let overlay = document.getElementById('anomalous-generated-gallery-overlay');
|
|
if (!overlay) {
|
|
overlay = document.createElement('div');
|
|
overlay.id = 'anomalous-generated-gallery-overlay';
|
|
overlay.style.position = 'absolute';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100%';
|
|
overlay.style.height = '100%';
|
|
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
|
|
overlay.style.zIndex = '999999';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
|
|
const modalBox = document.createElement('div');
|
|
modalBox.id = 'anomalous-generated-gallery-modal';
|
|
modalBox.style.width = '95%';
|
|
modalBox.style.maxHeight = '95%';
|
|
modalBox.style.backgroundColor = 'var(--comfy-menu-bg, var(--amb-bg-card))';
|
|
modalBox.style.borderRadius = '12px';
|
|
modalBox.style.display = 'flex';
|
|
modalBox.style.flexDirection = 'column';
|
|
modalBox.style.overflow = 'hidden';
|
|
modalBox.style.boxShadow = '0 10px 40px rgba(0, 0, 0, 0.8)';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.padding = '15px 25px';
|
|
header.style.background = 'var(--amb-bg-card-hover)';
|
|
header.style.display = 'flex';
|
|
header.style.justifyContent = 'space-between';
|
|
header.style.alignItems = 'center';
|
|
header.style.borderBottom = '1px solid var(--amb-border-strong)';
|
|
|
|
const title = document.createElement('h2');
|
|
title.id = 'anomalous-generated-gallery-title';
|
|
title.style.margin = '0';
|
|
title.style.color = '#fff';
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.textContent = `✖ ${t('galleryClose')}`;
|
|
closeBtn.style.padding = '8px 15px';
|
|
closeBtn.style.background = '#dc3545';
|
|
closeBtn.style.color = '#fff';
|
|
closeBtn.style.border = 'none';
|
|
closeBtn.style.borderRadius = '5px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.style.fontWeight = 'bold';
|
|
closeBtn.onmouseover = () => closeBtn.style.background = '#c82333';
|
|
closeBtn.onmouseout = () => closeBtn.style.background = '#dc3545';
|
|
closeBtn.onclick = () => {
|
|
overlay.style.display = 'none';
|
|
};
|
|
|
|
header.appendChild(title);
|
|
header.appendChild(closeBtn);
|
|
modalBox.appendChild(header);
|
|
|
|
const contentCont = document.createElement('div');
|
|
contentCont.id = 'anomalous-generated-gallery-content';
|
|
contentCont.style.flex = '1';
|
|
contentCont.style.overflowY = 'auto';
|
|
contentCont.style.padding = '20px';
|
|
contentCont.style.display = 'grid';
|
|
contentCont.style.gridTemplateColumns = 'repeat(auto-fill, minmax(220px, 1fr))';
|
|
contentCont.style.gap = '25px';
|
|
contentCont.style.rowGap = '40px';
|
|
contentCont.style.alignContent = 'start';
|
|
modalBox.appendChild(contentCont);
|
|
|
|
overlay.appendChild(modalBox);
|
|
document.getElementById('anomalous-container').appendChild(overlay);
|
|
}
|
|
|
|
const title = document.getElementById('anomalous-generated-gallery-title');
|
|
title.textContent = t('galleryHistoryTitle', { name: model.name || model.filename });
|
|
|
|
const contentCont = document.getElementById('anomalous-generated-gallery-content');
|
|
contentCont.innerHTML = '';
|
|
|
|
const loading = document.createElement('div');
|
|
loading.textContent = t('galleryScanning');
|
|
loading.style.textAlign = 'center';
|
|
loading.style.gridColumn = '1 / -1';
|
|
loading.style.padding = '50px';
|
|
loading.style.color = '#aaa';
|
|
contentCont.appendChild(loading);
|
|
|
|
overlay.style.display = 'flex';
|
|
|
|
try {
|
|
const res = await fetch('/anomalous/model_images?model_name=' + encodeURIComponent(model.filename) + '&t=' + Date.now());
|
|
const data = await res.json();
|
|
contentCont.innerHTML = '';
|
|
|
|
if (!data.images || data.images.length === 0) {
|
|
const emptyMsg = document.createElement('div');
|
|
emptyMsg.textContent = t('galleryNoModelImages');
|
|
emptyMsg.style.textAlign = 'center';
|
|
emptyMsg.style.gridColumn = '1 / -1';
|
|
emptyMsg.style.padding = '50px';
|
|
emptyMsg.style.color = '#888';
|
|
contentCont.appendChild(emptyMsg);
|
|
return;
|
|
}
|
|
|
|
data.images.forEach(img => {
|
|
const imgCont = document.createElement('div');
|
|
imgCont.className = 'anomalous-card';
|
|
imgCont.style.cursor = 'pointer';
|
|
|
|
const el = document.createElement('img');
|
|
el.src = img.url || img; // Support both just in case
|
|
el.loading = 'lazy';
|
|
el.draggable = true;
|
|
|
|
el.addEventListener('dragstart', (e) => {
|
|
const fullUrl = new URL(el.src, window.location.href).href;
|
|
e.dataTransfer.setData('text/uri-list', fullUrl);
|
|
e.dataTransfer.setData('text/plain', fullUrl);
|
|
if (window.anomalousDragGhostImg) {
|
|
e.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
|
|
}
|
|
});
|
|
|
|
let source_image = "";
|
|
let filenameText = "";
|
|
if (img.url) {
|
|
try {
|
|
const urlParams = new URLSearchParams(img.url.split('?')[1]);
|
|
filenameText = urlParams.get('filename') || '';
|
|
const sub = urlParams.get('subfolder') || '';
|
|
source_image = sub ? sub + '/' + filenameText : filenameText;
|
|
} catch (e) { }
|
|
} else {
|
|
filenameText = img.split('/').pop().split('?')[0];
|
|
source_image = filenameText;
|
|
}
|
|
|
|
const titleDiv = document.createElement('div');
|
|
titleDiv.className = 'anomalous-card-title';
|
|
titleDiv.innerText = filenameText;
|
|
|
|
const setCoverBtn = document.createElement('button');
|
|
setCoverBtn.textContent = t('gallerySetCover');
|
|
setCoverBtn.style.position = 'absolute';
|
|
setCoverBtn.style.bottom = '40px';
|
|
setCoverBtn.style.right = '5px';
|
|
setCoverBtn.style.background = 'rgba(40, 167, 69, 0.85)';
|
|
setCoverBtn.style.color = '#fff';
|
|
setCoverBtn.style.border = '1px solid var(--amb-border-strong)';
|
|
setCoverBtn.style.borderRadius = '4px';
|
|
setCoverBtn.style.padding = '4px 8px';
|
|
setCoverBtn.style.cursor = 'pointer';
|
|
setCoverBtn.style.zIndex = '10';
|
|
setCoverBtn.style.fontSize = '12px';
|
|
|
|
setCoverBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
if (!confirm(t('gallerySetCoverConfirm'))) return;
|
|
|
|
fetch('/anomalous/set_custom_cover', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename,
|
|
source_image: source_image
|
|
})
|
|
}).then(res => res.json()).then(async result => {
|
|
if (result.status === 'success') {
|
|
alert(t('galleryCoverSuccess'));
|
|
await this.loadModels();
|
|
const updatedModel = this.models.find(m => m.filename === model.filename);
|
|
if (updatedModel && this.currentDetailModel && this.currentDetailModel.filename === model.filename) {
|
|
this.showDetail(updatedModel);
|
|
}
|
|
} else {
|
|
alert(t('galleryErrorPrefix') + result.message);
|
|
}
|
|
}).catch(err => {
|
|
alert(t('galleryErrorPrefix') + err.message);
|
|
});
|
|
};
|
|
|
|
imgCont.onclick = () => {
|
|
const allGenCards = Array.from(contentCont.querySelectorAll('.anomalous-card'));
|
|
const curIndex = allGenCards.indexOf(imgCont);
|
|
const genItems = (data.images || []).map(im => {
|
|
const u = im.url || im;
|
|
let fn = '';
|
|
let sub = '';
|
|
if (im.url) {
|
|
try {
|
|
const up = new URLSearchParams(im.url.split('?')[1]);
|
|
fn = up.get('filename') || '';
|
|
sub = up.get('subfolder') || '';
|
|
} catch (_) {}
|
|
} else {
|
|
fn = String(im).split('/').pop().split('?')[0];
|
|
}
|
|
return {
|
|
filename: fn,
|
|
subfolder: sub,
|
|
url: u,
|
|
sourceImage: { type: 'output', filename: fn, subfolder: sub }
|
|
};
|
|
});
|
|
void showImageMaterialDetail(this, {
|
|
type: 'output',
|
|
filename: filenameText,
|
|
subfolder: source_image.includes('/') ? source_image.split('/')[0] : '',
|
|
}, img.url || img, {
|
|
items: genItems,
|
|
currentIndex: curIndex >= 0 ? curIndex : 0,
|
|
});
|
|
};
|
|
|
|
imgCont.appendChild(el);
|
|
imgCont.appendChild(setCoverBtn);
|
|
imgCont.appendChild(titleDiv);
|
|
contentCont.appendChild(imgCont);
|
|
});
|
|
} catch (e) {
|
|
const error = document.createElement('div');
|
|
error.textContent = t('galleryLoadImagesError');
|
|
error.style.color = 'red';
|
|
error.style.textAlign = 'center';
|
|
error.style.gridColumn = '1 / -1';
|
|
error.style.padding = '50px';
|
|
contentCont.replaceChildren(error);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function showGallerySelectMode(model) {
|
|
this.gallerySelectModel = model;
|
|
this.grid.style.display = 'none';
|
|
this.detailPanel.style.display = 'none';
|
|
this.galleryPanel.style.display = 'flex';
|
|
this.galleryPanel.classList.add('is-cover-selecting');
|
|
let banner = document.getElementById('anomalous-gallery-select-banner');
|
|
if (!banner) {
|
|
banner = document.createElement('div');
|
|
banner.id = 'anomalous-gallery-select-banner';
|
|
banner.style.background = '#28a745';
|
|
banner.style.color = '#fff';
|
|
banner.style.padding = '10px';
|
|
banner.style.textAlign = 'center';
|
|
banner.style.fontWeight = 'bold';
|
|
banner.style.position = 'sticky';
|
|
banner.style.top = '0';
|
|
banner.style.zIndex = '1000';
|
|
this.galleryPanel.insertBefore(banner, this.galleryPanel.firstChild);
|
|
}
|
|
banner.style.display = 'block';
|
|
banner.replaceChildren();
|
|
const bannerPrefix = document.createTextNode(t('gallerySelectingCoverPrefix'));
|
|
const bannerModel = document.createElement('span');
|
|
bannerModel.textContent = model.filename;
|
|
bannerModel.style.color = '#ff0';
|
|
const bannerSuffix = document.createTextNode(t('gallerySelectingCoverSuffix'));
|
|
const cancelSelect = document.createElement('button');
|
|
cancelSelect.id = 'anomalous-cancel-select';
|
|
cancelSelect.textContent = t('galleryCancel');
|
|
cancelSelect.style.marginLeft = '15px';
|
|
cancelSelect.style.color = '#000';
|
|
cancelSelect.style.background = '#fff';
|
|
cancelSelect.style.border = 'none';
|
|
cancelSelect.style.padding = '2px 8px';
|
|
cancelSelect.style.borderRadius = '4px';
|
|
cancelSelect.style.cursor = 'pointer';
|
|
banner.append(bannerPrefix, bannerModel, bannerSuffix, cancelSelect);
|
|
|
|
cancelSelect.onclick = () => {
|
|
const tempModel = this.gallerySelectModel;
|
|
this.gallerySelectModel = null;
|
|
this.galleryPanel.classList.remove('is-cover-selecting');
|
|
banner.style.display = 'none';
|
|
this.galleryPanel.style.display = 'none';
|
|
if (this.currentDetailModel) {
|
|
this.detailPanel.style.display = 'flex';
|
|
} else {
|
|
this.grid.style.display = 'grid';
|
|
}
|
|
if (tempModel) {
|
|
this.showEditModal(tempModel);
|
|
}
|
|
};
|
|
|
|
if (!this.galleryLoaded) {
|
|
this.loadGalleryImages(1, true);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function showGalleryViewer(src) {
|
|
let viewer = document.getElementById('anomalous-gallery-viewer');
|
|
if (!viewer) {
|
|
viewer = document.createElement('dialog');
|
|
viewer.id = 'anomalous-gallery-viewer';
|
|
viewer.className = 'anomalous-gallery-viewer';
|
|
|
|
const closeBtn = document.createElement('div');
|
|
closeBtn.className = 'anomalous-gallery-viewer-close';
|
|
closeBtn.innerHTML = '×';
|
|
|
|
const img = document.createElement('img');
|
|
img.id = 'anomalous-gallery-viewer-img';
|
|
|
|
viewer.appendChild(img);
|
|
viewer.appendChild(closeBtn);
|
|
|
|
let scale = 1;
|
|
let translateX = 0;
|
|
let translateY = 0;
|
|
let isDragging = false;
|
|
let startX = 0, startY = 0;
|
|
|
|
const resetImgTransform = () => {
|
|
scale = 1; translateX = 0; translateY = 0;
|
|
img.style.transform = `translate(0px, 0px) scale(1)`;
|
|
img.style.cursor = 'grab';
|
|
};
|
|
|
|
closeBtn.onclick = () => {
|
|
viewer.close();
|
|
resetImgTransform();
|
|
};
|
|
|
|
viewer.onclick = (e) => {
|
|
if (e.target === viewer) {
|
|
viewer.close();
|
|
resetImgTransform();
|
|
}
|
|
};
|
|
|
|
viewer.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
const zoomFactor = 0.1;
|
|
if (e.deltaY < 0) scale += zoomFactor;
|
|
else scale = Math.max(0.1, scale - zoomFactor);
|
|
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
});
|
|
|
|
img.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
startX = e.clientX - translateX;
|
|
startY = e.clientY - translateY;
|
|
img.style.cursor = 'grabbing';
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
translateX = e.clientX - startX;
|
|
translateY = e.clientY - startY;
|
|
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
|
});
|
|
|
|
window.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
img.style.cursor = 'grab';
|
|
});
|
|
|
|
document.body.appendChild(viewer);
|
|
}
|
|
|
|
const img = document.getElementById('anomalous-gallery-viewer-img');
|
|
img.src = src;
|
|
|
|
// Reset scale and translation when opening a new image
|
|
img.style.transform = `translate(0px, 0px) scale(1)`;
|
|
img.style.cursor = 'grab';
|
|
|
|
if (!viewer.open) viewer.showModal();
|
|
}
|