The user's two wishes for v1.58.2: - Dragging a gallery picture fades the browser out, as for the other cards (anomalous-material-dragging), so the canvas and the node under the pointer take the drop; it comes back on dragend. - Insert as a Load Image node closes the browser and the node follows the pointer until a click puts it down, Esc takes it off: a combo's Put on canvas (notebook_canvas.js's followPointer, now exported). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.8 KiB
JavaScript
113 lines
4.8 KiB
JavaScript
/**
|
|
* A gallery card's right-click menu: put the picture on the canvas as a Load Image node, or
|
|
* open its parameters. The picture is uploaded into the input folder the way a dropped file is
|
|
* (ComfyUI keeps one copy of the same picture): a "name.png [output]" value would be flagged
|
|
* as a missing file by ComfyUI's checks for any picture not made since ComfyUI started.
|
|
*/
|
|
|
|
import { app } from '../../../scripts/app.js';
|
|
import { translate as t } from './locales.js';
|
|
import { recordCanvasStep } from './canvas_history.js';
|
|
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
|
import { canvasPoint, followPointer } from './notebook_canvas.js';
|
|
|
|
let closeOpenMenu = null;
|
|
|
|
function row(icon, label, run, close) {
|
|
const item = document.createElement('button');
|
|
item.type = 'button';
|
|
item.className = 'anomalous-keep-row';
|
|
item.setAttribute('role', 'menuitem');
|
|
const mark = document.createElement('span');
|
|
mark.className = 'anomalous-keep-row-icon';
|
|
mark.textContent = icon;
|
|
const text = document.createElement('strong');
|
|
text.textContent = label;
|
|
item.append(mark, text);
|
|
item.onclick = (event) => {
|
|
close();
|
|
run(event);
|
|
};
|
|
return item;
|
|
}
|
|
|
|
/** The output picture uploaded into the input folder; its name there for a Load Image node. */
|
|
async function copyToInput(image) {
|
|
const query = new URLSearchParams({ filename: image.filename, subfolder: image.subfolder || '', type: 'output' });
|
|
const picture = await fetch(`/view?${query}`);
|
|
if (!picture.ok) throw new Error(`view ${picture.status}`);
|
|
const blob = await picture.blob();
|
|
const form = new FormData();
|
|
form.append('image', new File([blob], image.filename, { type: blob.type }));
|
|
form.append('type', 'input');
|
|
const response = await fetch('/upload/image', { method: 'POST', body: form });
|
|
if (!response.ok) throw new Error(`upload ${response.status}`);
|
|
const data = await response.json();
|
|
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name;
|
|
}
|
|
|
|
/**
|
|
* A Load Image node showing `image`: the browser closes and the node follows the pointer until a
|
|
* click puts it down (Esc takes it off again), like a combo's Put on canvas. `at` is the click
|
|
* that chose it, where the node starts.
|
|
*/
|
|
export async function insertLoadImageNode(owner, image, at) {
|
|
const node = globalThis.LiteGraph?.createNode('LoadImage');
|
|
const widget = node?.widgets?.find(item => item.name === 'image');
|
|
let file = null;
|
|
try {
|
|
if (node && widget && app.graph) file = await copyToInput(image);
|
|
} catch (error) {
|
|
console.error('[AMB] Could not copy the picture into the input folder:', error);
|
|
}
|
|
if (!file) {
|
|
showWorkbenchToast(t('galleryInsertNodeFailed'));
|
|
return;
|
|
}
|
|
const [x, y] = canvasPoint(at);
|
|
node.pos = [x - node.size[0] / 2, y - 20];
|
|
app.graph.add(node);
|
|
const choices = widget.options?.values;
|
|
if (Array.isArray(choices) && !choices.includes(file)) choices.push(file);
|
|
widget.value = file;
|
|
widget.callback?.(file); // shows the picture in the node
|
|
app.graph.setDirtyCanvas(true, true);
|
|
owner.close();
|
|
followPointer(owner, [{ node, relX: 0, relY: 0 }], () => recordCanvasStep(app));
|
|
showWorkbenchToast(t('galleryInsertNodeFollow'));
|
|
}
|
|
|
|
/** Opens the menu at the pointer; `openDetails()` opens the picture's parameters. */
|
|
export function openGalleryCardMenu(event, image, { owner, openDetails }) {
|
|
event.preventDefault();
|
|
closeOpenMenu?.();
|
|
const menu = document.createElement('div');
|
|
menu.className = 'anomalous-keep-menu anomalous-gallery-card-menu';
|
|
menu.setAttribute('role', 'menu');
|
|
const onPointer = (pointer) => { if (!menu.contains(pointer.target)) close(); };
|
|
const onKey = (key) => {
|
|
if (key.key !== 'Escape') return;
|
|
key.stopPropagation();
|
|
close();
|
|
};
|
|
const close = () => {
|
|
menu.remove();
|
|
window.removeEventListener('pointerdown', onPointer, true);
|
|
window.removeEventListener('keydown', onKey, true);
|
|
window.removeEventListener('wheel', close, true);
|
|
if (closeOpenMenu === close) closeOpenMenu = null;
|
|
};
|
|
menu.append(
|
|
row('🖼️', t('galleryInsertAsNode'), (click) => insertLoadImageNode(owner, image, click), close),
|
|
row('🔍', t('materialViewParameters'), openDetails, close),
|
|
);
|
|
document.body.append(menu);
|
|
const { width, height } = menu.getBoundingClientRect();
|
|
menu.style.left = `${Math.max(8, Math.min(event.clientX, window.innerWidth - width - 8))}px`;
|
|
menu.style.top = `${Math.max(8, Math.min(event.clientY, window.innerHeight - height - 8))}px`;
|
|
window.addEventListener('pointerdown', onPointer, true);
|
|
window.addEventListener('keydown', onKey, true);
|
|
window.addEventListener('wheel', close, true);
|
|
closeOpenMenu = close;
|
|
}
|