Esc and the shortcut close the browser; Show in folder; insert a picture as a node
From a Bilibili comment (2026-10-13):
- Esc closes the top layer: a dialog or menu (the plugin's alert and confirm
now close on Esc, as OK / Cancel), else the Workflows / Combos workspace,
else the browser. One capture listener decides before the layers act, so a
dialog closing on the same Esc still counts as above. The shortcut and the
Extensions entry toggle the browser.
- A model's file card has Show in folder: POST /anomalous/open_model_folder
(api/model_folder.py) takes type, folder index, subfolder and file name,
never a path, and opens Explorer / Finder with the file selected; a POST,
so local_guard keeps it to this computer. The card moves out of
ui_detail.js into model_location.js.
- A gallery card's right-click menu (gallery_card_menu.js): Insert as a Load
Image node reads the output file in place ("name.png [output]"), and View
Parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a23c3ec69d
commit
e0129eaa2f
+4
-1
@@ -83,6 +83,8 @@ covered by the `styles.css` manifest.
|
||||
and no scan replaces or deletes it.
|
||||
- `api/model_catalog.py`, `api/model_resolution.py`, `api/model_metadata.py`, and
|
||||
`api/model_media.py` own model listing, identity recovery, mutation, and covers;
|
||||
`api/model_folder.py` shows a model file in Explorer / Finder (by type, folder index and
|
||||
file name, never a path; a POST, so only this computer may ask);
|
||||
`api/models.py` is a compatibility facade; `api/model_constants.py` holds the
|
||||
model, media and sidecar extensions they share. `api/model_type_listing.py` lists a
|
||||
whole models folder with its subfolders for the models page's type chips.
|
||||
@@ -349,6 +351,7 @@ covered by the `styles.css` manifest.
|
||||
settings control that opens ComfyUI's keybinding editor.
|
||||
- `ui_model_sources.js` renders the Models page's Sources view (the last type chip): where each model is downloaded, for the open workflow or every model, with each link editable and saved in place; `model_source_links.js` owns its data and actions (collecting the workflow's models, resolving them here, saving a link to the model's information and the workflow, the canvas note and the clipboard list).
|
||||
- `ui_gallery_card.js` builds one output image's gallery card (viewer, workbench, cover pick, drag, star, delete);
|
||||
`gallery_card_menu.js` is its right-click menu (insert as a Load Image node reading the output file, parameters);
|
||||
`ui_keep_menu.js` is the star's keep menu (also the workbench's Keep): the whole workflow, the combo or the prompts,
|
||||
each saying where it goes, opening what was kept already or removing it (to the Recycle Bin through the recipe,
|
||||
notebook or material delete route); `image_keep.js` does the keeping (an image as a combo,
|
||||
@@ -376,7 +379,7 @@ covered by the `styles.css` manifest.
|
||||
card action buttons (one-click canvas addition with plus icon, model metadata editor, direct precision scanner without wizard modal popups)
|
||||
with absolute positioning cascades immune to tooltip target conflicts, vibrant hover affordance,
|
||||
safe docked sidebar preservation upon node addition, and multi-type node dispatch; `ui_detail.js`
|
||||
coordinates detail display, `ui_model_editor.js` owns metadata editing, and
|
||||
coordinates detail display (the file card with Show in folder in `model_location.js`), `ui_model_editor.js` owns metadata editing, and
|
||||
`ui_model_selector.js` owns advanced selection. `ui_gallery.js` (cards in
|
||||
`ui_gallery_card.js`) and `ui_gallery_detail.js` own generated-image browsing and workbench lifecycle,
|
||||
with stage interaction in `ui_image_stage.js` and metadata tabs in
|
||||
|
||||
+6
-1
@@ -1,6 +1,11 @@
|
||||
# 📈 Anomalous Model Browser Changelog
|
||||
|
||||
## v1.58.2 Beta (Fix: switching pictures with the keyboard) — unreleased
|
||||
## v1.58.2 Beta (Small fixes: Esc closes, Show in folder, insert a picture as a node) — unreleased
|
||||
|
||||
### 💬 From a Bilibili comment (来自 B 站评论)
|
||||
- **Esc closes the browser (Esc 关闭浏览器)**: Esc closes what is on top: a dialog or menu first, then the Workflows / Combos workspace, then the browser. The shortcut (Ctrl + Shift + M by default) and the Extensions menu entry now close the browser when it is open. Esc in a text box still belongs to the text box.
|
||||
- **Show in folder (打开所在文件夹)**: a model's details have **Show in folder** next to **Copy path**: Explorer (or Finder) opens with the file selected. Only on the computer ComfyUI runs on.
|
||||
- **Insert a picture as a node (图片作为节点插入)**: right-click a gallery picture → **Insert as a Load Image node** puts a Load Image node showing that picture in the middle of the canvas. It reads the picture from the output folder; nothing is copied.
|
||||
|
||||
### ⌨️ Arrow keys in picture details (查看参数里方向键切图)
|
||||
- In a picture's details, **← / →** (and **A / D**) switch pictures again after you ticked a node's checkbox under **All parameters**; the ticked checkbox kept the keyboard and the keys did nothing until you clicked elsewhere. Typing in a text box still keeps the arrows for the text.
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
from . import (
|
||||
activity_log, audio_catalog, folder_types, gallery_routes, materials, media_routes, model_catalog,
|
||||
model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
|
||||
model_folder, model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
|
||||
local_guard, translation_routes, version_manager,
|
||||
)
|
||||
from .scanner import *
|
||||
@@ -48,6 +48,7 @@ def setup_routes(app):
|
||||
app.router.add_get('/anomalous/config', api_get_config)
|
||||
app.router.add_post('/anomalous/save_config', api_save_config)
|
||||
app.router.add_post('/anomalous/delete_model', model_metadata.api_delete_model)
|
||||
app.router.add_post('/anomalous/open_model_folder', model_folder.api_open_model_folder)
|
||||
app.router.add_get('/anomalous/compatible_models', model_catalog.api_compatible_models)
|
||||
app.router.add_get('/anomalous/notebooks', api_get_notebooks)
|
||||
app.router.add_post('/anomalous/save_notebook', api_save_notebook)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Shows a model file in this computer's file manager: Explorer or Finder with the file
|
||||
selected, the folder on Linux. The request names the model the way the browser lists it
|
||||
(type, folder index, subfolder, file name), never a path, and local_guard lets only this
|
||||
computer ask (it is a POST)."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .utils import require_filename, resolve_folder_subdir, resolve_within
|
||||
|
||||
|
||||
def _reveal(path):
|
||||
if sys.platform == "win32":
|
||||
subprocess.Popen(f'explorer /select,"{path}"')
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.Popen(["open", "-R", path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", os.path.dirname(path)])
|
||||
|
||||
|
||||
async def api_open_model_folder(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
data = None
|
||||
if not isinstance(data, dict):
|
||||
return web.json_response({"status": "error", "message": "Invalid request body"}, status=400)
|
||||
try:
|
||||
filename = require_filename(data.get("filename", ""))
|
||||
_, folder = resolve_folder_subdir(data.get("type", ""), int(data.get("path_idx", 0)), data.get("subfolder", "/"))
|
||||
path = resolve_within(folder, filename)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return web.json_response({"status": "error", "message": "Invalid request parameters"}, status=400)
|
||||
if not os.path.isfile(path):
|
||||
return web.json_response({"status": "error", "message": "Model file not found"}, status=404)
|
||||
try:
|
||||
_reveal(path)
|
||||
except OSError as error:
|
||||
return web.json_response({"status": "error", "message": str(error)}, status=500)
|
||||
return web.json_response({"status": "success"})
|
||||
@@ -155,6 +155,13 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
|
||||
ensureBrowser()?.show();
|
||||
}
|
||||
|
||||
// The shortcut and the Extensions menu entry: open, or close when it is open.
|
||||
function toggle() {
|
||||
const browser = ensureBrowser();
|
||||
if (browser?.modal?.classList.contains('visible')) browser.close();
|
||||
else browser?.show();
|
||||
}
|
||||
|
||||
// The second shortcut (its ids still say materials) opens Prompt Studio.
|
||||
async function openMaterials() {
|
||||
const browser = ensureBrowser();
|
||||
@@ -464,7 +471,7 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
|
||||
onClick: open
|
||||
}],
|
||||
commands: [
|
||||
{ id: OPEN_BROWSER_COMMAND_ID, label: t('mainOpenTitle'), function: open },
|
||||
{ id: OPEN_BROWSER_COMMAND_ID, label: t('mainOpenTitle'), function: toggle },
|
||||
{ id: OPEN_MATERIALS_COMMAND_ID, label: t('mainOpenMaterialsTitle'), function: openMaterials },
|
||||
{ id: RESET_TRIGGER_POSITION_COMMAND_ID, label: t('mainResetFloatingTriggerPosition'), function: resetPosition }
|
||||
],
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* A gallery card's right-click menu: put the picture on the canvas as a Load Image node, or
|
||||
* open its parameters. The node reads the output file where it is ("name.png [output]",
|
||||
* ComfyUI's own way to name an output file), so nothing is copied into the input folder.
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
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 = () => {
|
||||
close();
|
||||
run();
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
/** A Load Image node in the middle of the visible canvas, showing `image` from the output folder. */
|
||||
export function insertLoadImageNode(image) {
|
||||
const node = globalThis.LiteGraph?.createNode('LoadImage');
|
||||
const widget = node?.widgets?.find(item => item.name === 'image');
|
||||
if (!node || !widget || !app.graph) {
|
||||
showWorkbenchToast(t('galleryInsertNodeFailed'));
|
||||
return;
|
||||
}
|
||||
const [x, y, width, height] = app.canvas?.visible_area || [0, 0, 0, 0];
|
||||
node.pos = [x + width / 2 - node.size[0] / 2, y + height / 2 - node.size[1] / 2];
|
||||
app.graph.add(node);
|
||||
const file = image.subfolder ? `${image.subfolder}/${image.filename}` : image.filename;
|
||||
widget.value = `${file} [output]`;
|
||||
widget.callback?.(widget.value); // shows the picture in the node
|
||||
app.canvas?.selectNode?.(node);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
recordCanvasStep(app);
|
||||
showWorkbenchToast(t('galleryInsertNodeDone'));
|
||||
}
|
||||
|
||||
/** Opens the menu at the pointer; `openDetails()` opens the picture's parameters. */
|
||||
export function openGalleryCardMenu(event, image, { 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'), () => insertLoadImageNode(image), 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;
|
||||
}
|
||||
@@ -599,6 +599,11 @@ export const i18n = {
|
||||
detailCopyPath: '复制路径',
|
||||
detailPathCopied: '已复制',
|
||||
detailPathCopyFailed: '复制失败',
|
||||
detailOpenFolder: '打开所在文件夹',
|
||||
detailOpenFolderFailed: '打不开文件夹:{error}',
|
||||
galleryInsertAsNode: '作为“加载图像”节点插入工作流',
|
||||
galleryInsertNodeDone: '已在画布中间放了一个“加载图像”节点',
|
||||
galleryInsertNodeFailed: '没能放入“加载图像”节点(画布还没准备好?)',
|
||||
detailCustomNamePlaceholder: '自定义名称 (留空则使用原名)',
|
||||
detailNotesPlaceholder: '📓 专属备注... (支持多行)',
|
||||
detailPhysicalRename: '同步物理重命名',
|
||||
@@ -2502,6 +2507,11 @@ export const i18n = {
|
||||
detailCopyPath: 'Copy path',
|
||||
detailPathCopied: 'Copied',
|
||||
detailPathCopyFailed: 'Copy failed',
|
||||
detailOpenFolder: 'Show in folder',
|
||||
detailOpenFolderFailed: 'Could not open the folder: {error}',
|
||||
galleryInsertAsNode: 'Insert as a Load Image node',
|
||||
galleryInsertNodeDone: 'A Load Image node is in the middle of the canvas',
|
||||
galleryInsertNodeFailed: 'Could not add a Load Image node (is the canvas ready?)',
|
||||
detailCustomNamePlaceholder: 'Custom Name (Leave empty to use original)',
|
||||
detailNotesPlaceholder: '📓 Custom Notes...',
|
||||
detailPhysicalRename: 'Physical Rename on Disk',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* The model details' location card: the file name and full path, Copy path, and Show in
|
||||
* folder (Explorer / Finder opens with the file selected; only on the computer ComfyUI runs on).
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { anomalousAlert } from './ui_dialog.js';
|
||||
|
||||
function actionButton(label) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'anomalous-model-location-copy';
|
||||
button.textContent = label;
|
||||
return button;
|
||||
}
|
||||
|
||||
async function showInFolder(model, where) {
|
||||
try {
|
||||
const response = await fetch('/anomalous/open_model_folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...where, filename: model.filename }),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (data.status !== 'success') throw new Error(data.message || `HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
await anomalousAlert(t('detailOpenFolderFailed', { error: error.message }));
|
||||
}
|
||||
}
|
||||
|
||||
/** `where`: the model's { type, path_idx, subfolder } in the browser. */
|
||||
export function createModelLocationCard(model, where) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'anomalous-model-location';
|
||||
|
||||
const appendValue = (labelKey, value) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'anomalous-model-location-row';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'anomalous-model-location-label';
|
||||
label.textContent = t(labelKey);
|
||||
const content = document.createElement('code');
|
||||
content.textContent = value;
|
||||
content.title = value;
|
||||
row.append(label, content);
|
||||
card.appendChild(row);
|
||||
};
|
||||
|
||||
appendValue('detailPhysicalFilename', model.filename);
|
||||
if (model.file_path) {
|
||||
appendValue('detailFullPath', model.file_path);
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'anomalous-model-location-actions';
|
||||
const copyButton = actionButton(t('detailCopyPath'));
|
||||
copyButton.onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(model.file_path);
|
||||
copyButton.textContent = t('detailPathCopied');
|
||||
} catch (error) {
|
||||
copyButton.textContent = t('detailPathCopyFailed');
|
||||
}
|
||||
setTimeout(() => { copyButton.textContent = t('detailCopyPath'); }, 1400);
|
||||
};
|
||||
const folderButton = actionButton(t('detailOpenFolder'));
|
||||
folderButton.onclick = () => showInFolder(model, where);
|
||||
actions.append(copyButton, folderButton);
|
||||
card.appendChild(actions);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
@@ -57,21 +57,24 @@ export function closeWorkspace() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Esc on the recipe / combo workspace does what a click beside it does:
|
||||
* `closeWorkspace()`. Not while one of this plugin's dialogs or a ComfyUI dialog sits
|
||||
* above it, and not while a text field has the key (Esc there belongs to the field).
|
||||
* ComfyUI's own keybindings mark Esc as handled, so `defaultPrevented` says nothing here.
|
||||
* Esc closes the browser's top layer: the recipe / combo workspace when it is open (what a
|
||||
* click beside it does), else the browser. Not while one of this plugin's dialogs, overlays
|
||||
* or menus, or a ComfyUI dialog, sits above it (they close themselves), and not while a text
|
||||
* field has the key (Esc there belongs to the field). It listens first (capture), so a dialog
|
||||
* that closes on this same Esc still counts as above. ComfyUI's own keybindings mark Esc as
|
||||
* handled, so `defaultPrevented` says nothing here.
|
||||
*/
|
||||
export function bindWorkspaceEscape(owner) {
|
||||
export function bindBrowserEscape(owner) {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (!owner.modal?.classList.contains('visible') || owner.nbPanel?.style.display !== 'flex') return;
|
||||
if (e.key !== 'Escape' || !owner.modal?.classList.contains('visible')) return;
|
||||
if (e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
||||
const layered = [...document.body.children, ...owner.nbPanel.children]
|
||||
.some(el => [...el.classList].some(c => c.startsWith('anomalous-') && c.endsWith('overlay')) && el.getClientRects().length);
|
||||
const layered = [...document.body.children, ...(owner.nbPanel?.children || [])]
|
||||
.some(el => el !== owner.modal && el.getClientRects().length
|
||||
&& (el.getAttribute('role') === 'menu' || [...el.classList].some(c => c.startsWith('anomalous-') && c.endsWith('overlay'))));
|
||||
if (layered || document.querySelector('dialog[open], .p-dialog-mask')) return;
|
||||
owner.closeWorkspace();
|
||||
});
|
||||
if (owner.nbPanel?.style.display === 'flex') owner.closeWorkspace();
|
||||
else owner.close();
|
||||
}, true);
|
||||
}
|
||||
|
||||
export function hideAllPanels() {
|
||||
|
||||
@@ -7,49 +7,10 @@ import { app } from "../../../scripts/app.js";
|
||||
import { translate } from './locales.js';
|
||||
import { escapeHtml, setSafeRichHtml } from './safe_dom.js';
|
||||
import { renderSourceLine } from './model_source.js';
|
||||
import { createModelLocationCard } from './model_location.js';
|
||||
|
||||
const t = (key, params) => translate(key, params);
|
||||
|
||||
function createModelLocationCard(model) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'anomalous-model-location';
|
||||
|
||||
const appendValue = (labelKey, value) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'anomalous-model-location-row';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'anomalous-model-location-label';
|
||||
label.textContent = t(labelKey);
|
||||
const content = document.createElement('code');
|
||||
content.textContent = value;
|
||||
content.title = value;
|
||||
row.append(label, content);
|
||||
card.appendChild(row);
|
||||
};
|
||||
|
||||
appendValue('detailPhysicalFilename', model.filename);
|
||||
if (model.file_path) {
|
||||
appendValue('detailFullPath', model.file_path);
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.type = 'button';
|
||||
copyButton.className = 'anomalous-model-location-copy';
|
||||
copyButton.textContent = t('detailCopyPath');
|
||||
copyButton.onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(model.file_path);
|
||||
copyButton.textContent = t('detailPathCopied');
|
||||
} catch (error) {
|
||||
copyButton.textContent = t('detailPathCopyFailed');
|
||||
}
|
||||
setTimeout(() => { copyButton.textContent = t('detailCopyPath'); }, 1400);
|
||||
};
|
||||
card.appendChild(copyButton);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function showDetail(model) {
|
||||
if (this.currentDetailObserver) {
|
||||
this.currentDetailObserver.disconnect();
|
||||
@@ -389,7 +350,9 @@ export function showDetail(model) {
|
||||
topRow.appendChild(editMetaBtn);
|
||||
|
||||
rightPanel.appendChild(topRow);
|
||||
rightPanel.appendChild(createModelLocationCard(model));
|
||||
rightPanel.appendChild(createModelLocationCard(model, {
|
||||
type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder,
|
||||
}));
|
||||
|
||||
// 1.5 Custom Notes Section (Google Material Card)
|
||||
if (m.custom_notes) {
|
||||
|
||||
@@ -62,6 +62,11 @@ export function anomalousAlert(message, title = 'Anomalous') {
|
||||
};
|
||||
|
||||
okBtn.onclick = cleanup;
|
||||
overlay.onkeydown = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
e.stopPropagation();
|
||||
cleanup();
|
||||
};
|
||||
|
||||
footer.appendChild(okBtn);
|
||||
dialog.append(heading, text, footer);
|
||||
@@ -144,6 +149,11 @@ export function anomalousConfirm(message, title = 'Anomalous', options = {}) {
|
||||
cancelBtn.onclick = () => finish(null);
|
||||
noBtn?.addEventListener('click', () => finish(false));
|
||||
okBtn.onclick = () => finish(true);
|
||||
overlay.onkeydown = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
e.stopPropagation();
|
||||
finish(null); // as Cancel
|
||||
};
|
||||
|
||||
footer.append(cancelBtn);
|
||||
if (noBtn) footer.append(noBtn);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* 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).
|
||||
* filled once kept as anything), Delete (to the Recycle Bin, after a confirmation) and the
|
||||
* right-click menu (gallery_card_menu.js: insert as a Load Image node, parameters).
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
@@ -10,6 +11,7 @@ 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';
|
||||
import { openGalleryCardMenu } from './gallery_card_menu.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>';
|
||||
@@ -138,6 +140,9 @@ export function createGalleryCard(owner, imgData, { showViewer }) {
|
||||
if (window.anomalousDragGhostImg) event.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
|
||||
});
|
||||
img.onclick = () => (owner.gallerySelectModel ? useAsCover(owner, image) : showViewer(url));
|
||||
card.addEventListener('contextmenu', (event) => {
|
||||
if (!owner.gallerySelectModel) openGalleryCardMenu(event, image, { openDetails: () => openWorkbench(owner, image, url) });
|
||||
});
|
||||
|
||||
const details = button('anomalous-gallery-details', t('materialViewDetails'), () => {
|
||||
if (!owner.gallerySelectModel) openWorkbench(owner, image, url);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { renderRailTools } from './ui_rail_tools.js';
|
||||
import { getActiveDomain } from './ui_domain_switcher.js';
|
||||
import { renderAudioSidebar } from './ui_audio_sidebar.js';
|
||||
import { createGallerySearchBar } from './ui_gallery.js';
|
||||
import { bindWorkspaceEscape } from './ui_browser_navigation.js';
|
||||
import { bindBrowserEscape } from './ui_browser_navigation.js';
|
||||
import { createShellRail } from './ui_shell_rail.js';
|
||||
import { installShellNavigation } from './ui_shell_nav.js';
|
||||
import { bindShellDrag, bindShellResize } from './ui_shell_frame.js';
|
||||
@@ -216,7 +216,7 @@ export function createDOM() {
|
||||
this.nbPanel.onclick = (e) => {
|
||||
if (e.target === this.nbPanel) this.closeWorkspace();
|
||||
};
|
||||
bindWorkspaceEscape(this);
|
||||
bindBrowserEscape(this);
|
||||
|
||||
content.append(header, this.grid, this.detailPanel, this.galleryPanel, this.homePanel, this.activityPanel, this.scanPanel, this.settingsPanel, this.doctorPanel,
|
||||
this.assistantPanel, this.audioStudioPanel, this.scriptPanel, this.audioGalleryPanel);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import url("./styles/00-foundation-models.css?v=20261003-settings-1");
|
||||
@import url("./styles/01-shell-tools.css?v=20261003-settings-1");
|
||||
@import url("./styles/02-notebooks.css?v=20261009-click-1");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261009-click-1");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261013-menu-1");
|
||||
@import url("./styles/04-dialogs-materials.css?v=20261004-studio-1");
|
||||
@import url("./styles/05-recipe-studio-overrides.css?v=20261002-theme-1");
|
||||
@import url("./styles/06-material-workbench.css?v=20261004-studio-1");
|
||||
|
||||
@@ -198,7 +198,13 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The keep menu under a star or the workbench's Keep (ui_keep_menu.js). */
|
||||
/* The keep menu under a star or the workbench's Keep (ui_keep_menu.js); a gallery card's
|
||||
right-click menu (gallery_card_menu.js) wears the same look, as wide as its rows. */
|
||||
.anomalous-gallery-card-menu {
|
||||
width: max-content;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.anomalous-keep-menu {
|
||||
position: fixed;
|
||||
z-index: 1000001;
|
||||
@@ -1213,6 +1219,12 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.anomalous-model-location-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-model-location-copy {
|
||||
justify-self: start;
|
||||
padding: 4px 9px;
|
||||
|
||||
Reference in New Issue
Block a user