bugfix: correct event handling in grid and sidebar
This commit is contained in:
@@ -52,6 +52,13 @@ export function handleDragStart(file, ev) {
|
||||
ev.dataTransfer.effectAllowed = "copy";
|
||||
ev.dataTransfer.setData("text/uri-list", url);
|
||||
ev.dataTransfer.setData("DownloadURL", `${mime}:${filename}:${url}`);
|
||||
|
||||
if (kind === "video") {
|
||||
ev.dataTransfer.setData(
|
||||
"application/x-mjr-asset",
|
||||
JSON.stringify({ filename, subfolder: file.subfolder || "", kind: "video" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateWorkflowDot(card, workflowState) {
|
||||
|
||||
+275
-1
@@ -1,3 +1,4 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { buildViewUrl, detectKindFromExt, getExt, mjrSettings, mjrShowToast } from "../ui_settings.js";
|
||||
import { mjrGlobalState } from "../mjr_global.js";
|
||||
@@ -5,12 +6,18 @@ import { sortFilesDeterministically } from "../am_data.js";
|
||||
|
||||
let mjrGlobalRefreshTimer = null;
|
||||
let mjrFocusListenerAttached = false;
|
||||
let mjrVideoNodeDropBridgeBound = false;
|
||||
let mjrQueueListenerBound = false;
|
||||
let mjrNewFilesListenerBound = false;
|
||||
let mjrCapabilitiesCache = null;
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.MJR_DND_DEBUG === undefined) {
|
||||
window.MJR_DND_DEBUG = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
const mjrDbg = (...args) => {
|
||||
try {
|
||||
if (mjrSettings && mjrSettings.debugDnD) {
|
||||
if ((mjrSettings && mjrSettings.debugDnD) || (typeof window !== "undefined" && window.MJR_DND_DEBUG)) {
|
||||
console.debug("[Majoor.DnD]", ...args);
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -98,6 +105,273 @@ export function mjrDispatchSyntheticDrop(target, file) {
|
||||
}
|
||||
}
|
||||
|
||||
let mjrHighlightedNode = null;
|
||||
let mjrHighlightedNodePrev = null;
|
||||
|
||||
function getNodeUnderClientXY(clientX, clientY) {
|
||||
const canvasEl = app?.canvas?.canvas || document.querySelector("canvas");
|
||||
const graph = app?.canvas?.graph;
|
||||
const ds = app?.canvas?.ds;
|
||||
if (!canvasEl || !graph || !ds) return null;
|
||||
|
||||
const rect = canvasEl.getBoundingClientRect();
|
||||
if (
|
||||
clientX < rect.left ||
|
||||
clientX > rect.right ||
|
||||
clientY < rect.top ||
|
||||
clientY > rect.bottom
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scale = Number(ds.scale) || 1;
|
||||
const off = ds.offset || [0, 0];
|
||||
const offX = Array.isArray(off) ? Number(off[0]) || 0 : Number(off?.x) || 0;
|
||||
const offY = Array.isArray(off) ? Number(off[1]) || 0 : Number(off?.y) || 0;
|
||||
const x = (clientX - rect.left) / scale - offX;
|
||||
const y = (clientY - rect.top) / scale - offY;
|
||||
|
||||
if (typeof graph.getNodeOnPos === "function") return graph.getNodeOnPos(x, y);
|
||||
|
||||
const nodes = graph._nodes || [];
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const n = nodes[i];
|
||||
if (!n?.pos || !n?.size) continue;
|
||||
if (
|
||||
x >= n.pos[0] &&
|
||||
y >= n.pos[1] &&
|
||||
x <= n.pos[0] + n.size[0] &&
|
||||
y <= n.pos[1] + n.size[1]
|
||||
) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isVhsVideoNode(node) {
|
||||
if (!node) return false;
|
||||
const title = String(node.title || "");
|
||||
const type = String(node.type || "");
|
||||
if (/video/i.test(title)) return true;
|
||||
if (type.includes("VHS_LoadVideo")) return true;
|
||||
|
||||
const widgets = node.widgets;
|
||||
if (Array.isArray(widgets)) {
|
||||
return widgets.some((w) => String(w?.name || "").toLowerCase() === "video");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function markCanvasDirty() {
|
||||
try {
|
||||
app?.graph?.setDirtyCanvas?.(true, true);
|
||||
} catch (_) {}
|
||||
try {
|
||||
app?.canvas?.setDirty?.(true, true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function applyHighlight(node) {
|
||||
if (!node) return;
|
||||
if (mjrHighlightedNode === node) return;
|
||||
|
||||
clearHighlight();
|
||||
mjrHighlightedNode = node;
|
||||
mjrHighlightedNodePrev = { color: node.color, bgcolor: node.bgcolor };
|
||||
node.bgcolor = "#3355ff";
|
||||
node.color = "#a9c4ff";
|
||||
markCanvasDirty();
|
||||
}
|
||||
|
||||
function clearHighlight() {
|
||||
if (!mjrHighlightedNode) return;
|
||||
try {
|
||||
if (mjrHighlightedNodePrev) {
|
||||
mjrHighlightedNode.color = mjrHighlightedNodePrev.color;
|
||||
mjrHighlightedNode.bgcolor = mjrHighlightedNodePrev.bgcolor;
|
||||
}
|
||||
} catch (_) {}
|
||||
mjrHighlightedNode = null;
|
||||
mjrHighlightedNodePrev = null;
|
||||
markCanvasDirty();
|
||||
}
|
||||
|
||||
function ensureComboHasValue(widget, value) {
|
||||
if (!widget || widget.type !== "combo" || !widget.options) return;
|
||||
|
||||
let vals = null;
|
||||
let target = null;
|
||||
if (Array.isArray(widget.options.values)) {
|
||||
vals = widget.options.values;
|
||||
target = widget.options;
|
||||
} else if (widget.options.values && Array.isArray(widget.options.values.values)) {
|
||||
vals = widget.options.values.values;
|
||||
target = widget.options.values;
|
||||
}
|
||||
if (!Array.isArray(vals) || !target) return;
|
||||
|
||||
const has = vals.some((v) => {
|
||||
if (typeof v === "string") return v === value;
|
||||
return (v?.content ?? v?.value ?? v?.text) === value;
|
||||
});
|
||||
if (has) return;
|
||||
|
||||
if (vals.length === 0 || typeof vals[0] === "string") vals.unshift(value);
|
||||
else vals.unshift({ content: value, text: value, value });
|
||||
target.values = vals;
|
||||
}
|
||||
|
||||
function pickVideoWidget(node) {
|
||||
const widgets = node?.widgets;
|
||||
if (!Array.isArray(widgets) || !widgets.length) return null;
|
||||
const exact = widgets.find((w) => String(w?.name || "").toLowerCase() === "video");
|
||||
if (exact) return exact;
|
||||
return (
|
||||
widgets.find((w) => /video|file|path/i.test(String(w?.name || w?.label || ""))) || null
|
||||
);
|
||||
}
|
||||
|
||||
function comboHasValue(widget, value) {
|
||||
if (!widget || widget.type !== "combo" || !widget.options) return false;
|
||||
const vals =
|
||||
(Array.isArray(widget.options.values) && widget.options.values) ||
|
||||
(widget.options.values && Array.isArray(widget.options.values.values) && widget.options.values.values) ||
|
||||
null;
|
||||
if (!Array.isArray(vals)) return false;
|
||||
return vals.some((v) => {
|
||||
if (typeof v === "string") return v === value;
|
||||
return (v?.content ?? v?.value ?? v?.text) === value;
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureVideoNodeDropBridge() {
|
||||
if (mjrVideoNodeDropBridgeBound) return () => {};
|
||||
|
||||
const onDragOver = (e) => {
|
||||
const types = Array.from(e?.dataTransfer?.types || []);
|
||||
if (!types.includes("application/x-mjr-asset")) return;
|
||||
|
||||
let payload = null;
|
||||
try {
|
||||
payload = JSON.parse(e.dataTransfer.getData("application/x-mjr-asset"));
|
||||
} catch (_) {
|
||||
payload = null;
|
||||
}
|
||||
if (!payload || payload.kind !== "video") return;
|
||||
|
||||
const node = getNodeUnderClientXY(e.clientX, e.clientY);
|
||||
if (node && isVhsVideoNode(node)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation?.();
|
||||
e.stopPropagation();
|
||||
applyHighlight(node);
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
mjrDbg("DnD video dragover", { payload, nodeTitle: node?.title, nodeType: node?.type });
|
||||
return;
|
||||
}
|
||||
|
||||
clearHighlight();
|
||||
// Pass-through: let ComfyUI handle canvas-friendly video drop (workflow embed).
|
||||
};
|
||||
|
||||
const onDrop = async (e) => {
|
||||
const types = Array.from(e?.dataTransfer?.types || []);
|
||||
if (!types.includes("application/x-mjr-asset")) return;
|
||||
|
||||
let payload = null;
|
||||
try {
|
||||
payload = JSON.parse(e.dataTransfer.getData("application/x-mjr-asset"));
|
||||
} catch (_) {
|
||||
payload = null;
|
||||
}
|
||||
if (!payload || payload.kind !== "video") return;
|
||||
|
||||
const node = getNodeUnderClientXY(e.clientX, e.clientY);
|
||||
if (!node || !isVhsVideoNode(node)) {
|
||||
clearHighlight();
|
||||
mjrDbg("DnD video drop pass-through", { payload, nodeTitle: node?.title, nodeType: node?.type });
|
||||
return; // leave ComfyUI to load workflow from video metadata
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation?.();
|
||||
e.stopPropagation();
|
||||
clearHighlight();
|
||||
|
||||
const filename = payload.filename;
|
||||
const subfolder = payload.subfolder || "";
|
||||
if (!filename) return;
|
||||
|
||||
let stageResp = null;
|
||||
try {
|
||||
const res = await api.fetchApi("/mjr/filemanager/stage_to_input", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, subfolder, from_type: "output" }),
|
||||
});
|
||||
stageResp = res.ok ? await res.json().catch(() => null) : null;
|
||||
} catch (_) {
|
||||
stageResp = null;
|
||||
}
|
||||
|
||||
const relativePath = stageResp?.relative_path;
|
||||
const stagedName = stageResp?.filename || stageResp?.name || filename;
|
||||
mjrDbg("DnD stage_to_input response", stageResp);
|
||||
|
||||
if (!relativePath && !stagedName) {
|
||||
mjrShowToast("error", "Failed to stage video to input", "Drag & Drop");
|
||||
return;
|
||||
}
|
||||
|
||||
const w = pickVideoWidget(node);
|
||||
if (!w) {
|
||||
mjrShowToast("warn", "No compatible 'video' widget found", "Drag & Drop");
|
||||
return;
|
||||
}
|
||||
|
||||
const prefer = w.type === "combo" && comboHasValue(w, relativePath) ? relativePath : null;
|
||||
const value =
|
||||
prefer ||
|
||||
(w.type === "combo" && comboHasValue(w, stagedName) ? stagedName : null) ||
|
||||
relativePath ||
|
||||
stagedName;
|
||||
|
||||
if (w.type === "combo") ensureComboHasValue(w, value);
|
||||
w.value = value;
|
||||
try {
|
||||
w.callback?.(w.value);
|
||||
} catch (_) {}
|
||||
|
||||
markCanvasDirty();
|
||||
|
||||
mjrDbg("DnD video injected", {
|
||||
payload,
|
||||
nodeTitle: node?.title,
|
||||
nodeType: node?.type,
|
||||
widget: { name: w?.name, type: w?.type },
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
const onDragLeave = (_e) => {
|
||||
clearHighlight();
|
||||
};
|
||||
|
||||
window.addEventListener("dragover", onDragOver, true);
|
||||
window.addEventListener("drop", onDrop, true);
|
||||
window.addEventListener("dragleave", onDragLeave, true);
|
||||
mjrVideoNodeDropBridgeBound = true;
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("dragover", onDragOver, true);
|
||||
window.removeEventListener("drop", onDrop, true);
|
||||
window.removeEventListener("dragleave", onDragLeave, true);
|
||||
mjrVideoNodeDropBridgeBound = false;
|
||||
clearHighlight();
|
||||
};
|
||||
}
|
||||
|
||||
export function mjrStartAutoRefreshTimer(refreshAllInstances) {
|
||||
if (mjrGlobalRefreshTimer) {
|
||||
clearInterval(mjrGlobalRefreshTimer);
|
||||
|
||||
+7
-5
@@ -11,10 +11,11 @@ import {
|
||||
mjrStepVideos,
|
||||
mjrUpdateNavVisibility,
|
||||
} from "./ui_viewer.js";
|
||||
import { createMetadataSidebar } from "./ui_sidebar.js";
|
||||
import { createGridView } from "./am_gridview.js";
|
||||
import { createMetadataSidebar } from "./ui_sidebar.js";
|
||||
import { createGridView } from "./am_gridview.js";
|
||||
import {
|
||||
ensureGlobalAutoRefresh,
|
||||
ensureVideoNodeDropBridge,
|
||||
ensureNewFilesListener,
|
||||
ensureQueueListener,
|
||||
mjrStartAutoRefreshTimer,
|
||||
@@ -177,10 +178,11 @@ function renderAssetsManager(root) {
|
||||
loadFiles,
|
||||
lastSignatureRef,
|
||||
} = createRefreshController(state, grid, applyFilterAndRender, fetchMetadataForVisible);
|
||||
refreshAllInstances = refreshAllInstancesFn;
|
||||
const refreshCollections = () => populateCollectionsOptions(state, collectionFilterSelect);
|
||||
|
||||
refreshAllInstances = refreshAllInstancesFn;
|
||||
const refreshCollections = () => populateCollectionsOptions(state, collectionFilterSelect);
|
||||
|
||||
// Media drops on the ComfyUI canvas are handled natively by ComfyUI (this plugin does not intercept drop events).
|
||||
cleanups.push(ensureVideoNodeDropBridge());
|
||||
cleanups.push(ensureQueueListener(refreshAllInstances));
|
||||
cleanups.push(ensureNewFilesListener());
|
||||
cleanups.push(ensureGlobalAutoRefresh(refreshAllInstances));
|
||||
|
||||
+8
-6
@@ -565,7 +565,7 @@ export function createMetadataSidebar(options) {
|
||||
params.set("filename", filename);
|
||||
if (file.subfolder) params.set("subfolder", file.subfolder);
|
||||
|
||||
showPanel(file, fallbackMeta, cachedEntry ? null : "Loading metadata…");
|
||||
showPanel(file, fallbackMeta, cachedEntry ? null : "Loading metadata...");
|
||||
|
||||
try {
|
||||
const res = await api.fetchApi(`/mjr/filemanager/metadata?${params.toString()}`);
|
||||
@@ -593,11 +593,13 @@ export function createMetadataSidebar(options) {
|
||||
} else {
|
||||
showPanel(file, null, data.error || "No metadata found for this file.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Majoor.AssetsManager] metadata error", err);
|
||||
showPanel(file, null, "Error while loading metadata (check console).");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (seq !== metaRequestSeq) return; // ignore stale request failures
|
||||
if (err?.name === "AbortError") return;
|
||||
console.error("[Majoor.AssetsManager] metadata error", err);
|
||||
showPanel(file, null, "Error while loading metadata (check console).");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadMetadataForFile,
|
||||
|
||||
+114
-57
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import asyncio
|
||||
import time
|
||||
@@ -65,6 +66,49 @@ except Exception:
|
||||
|
||||
_CACHE_LOCK = threading.Lock()
|
||||
|
||||
_ASYNCIO_SILENCER_INSTALLED = False
|
||||
|
||||
|
||||
def _install_windows_asyncio_connection_reset_silencer() -> None:
|
||||
"""
|
||||
On Windows, aborted HTTP requests can produce noisy asyncio logs like:
|
||||
"Exception in callback _ProactorBasePipeTransport._call_connection_lost()"
|
||||
with ConnectionResetError([WinError 10054]).
|
||||
|
||||
This does not affect correctness, but it can confuse users and pollute logs.
|
||||
"""
|
||||
global _ASYNCIO_SILENCER_INSTALLED
|
||||
if _ASYNCIO_SILENCER_INSTALLED:
|
||||
return
|
||||
if sys.platform != "win32":
|
||||
_ASYNCIO_SILENCER_INSTALLED = True
|
||||
return
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
previous = loop.get_exception_handler()
|
||||
|
||||
def _handler(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) -> None:
|
||||
exc = context.get("exception")
|
||||
msg = context.get("message") or ""
|
||||
if (
|
||||
isinstance(exc, ConnectionResetError)
|
||||
and getattr(exc, "winerror", None) == 10054
|
||||
and "_call_connection_lost" in str(msg)
|
||||
):
|
||||
return
|
||||
|
||||
if previous:
|
||||
previous(loop, context)
|
||||
else:
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
loop.set_exception_handler(_handler)
|
||||
_ASYNCIO_SILENCER_INSTALLED = True
|
||||
|
||||
|
||||
def _folder_changed(root_path: Path) -> bool:
|
||||
"""Quick check if folder changed (file added/modified)."""
|
||||
@@ -422,6 +466,7 @@ async def _scan_outputs_async(force: bool = False) -> List[Dict[str, Any]]:
|
||||
|
||||
@PromptServer.instance.routes.get("/mjr/filemanager/files")
|
||||
async def list_files(request: web.Request) -> web.Response:
|
||||
_install_windows_asyncio_connection_reset_silencer()
|
||||
force_param = (request.query.get("force") or "").lower()
|
||||
force = force_param in ("1", "true", "yes", "force")
|
||||
|
||||
@@ -471,9 +516,12 @@ async def list_files(request: web.Request) -> web.Response:
|
||||
|
||||
@PromptServer.instance.routes.post("/mjr/filemanager/metadata/batch")
|
||||
async def batch_metadata(request: web.Request) -> web.Response:
|
||||
_install_windows_asyncio_connection_reset_silencer()
|
||||
root = _get_output_root()
|
||||
try:
|
||||
payload = await request.json()
|
||||
except ConnectionResetError:
|
||||
return web.Response(status=499)
|
||||
except Exception:
|
||||
return web.json_response({"ok": False, "error": "Invalid JSON"}, status=400)
|
||||
|
||||
@@ -541,7 +589,10 @@ async def batch_metadata(request: web.Request) -> web.Response:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return web.json_response({"ok": True, "metadatas": results, "errors": errors})
|
||||
try:
|
||||
return web.json_response({"ok": True, "metadatas": results, "errors": errors})
|
||||
except ConnectionResetError:
|
||||
return web.Response(status=499)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/mjr/filemanager/delete")
|
||||
@@ -640,6 +691,7 @@ async def open_folder(request: web.Request) -> web.Response:
|
||||
|
||||
@PromptServer.instance.routes.get("/mjr/filemanager/metadata")
|
||||
async def get_metadata(request: web.Request) -> web.Response:
|
||||
_install_windows_asyncio_connection_reset_silencer()
|
||||
root = _get_output_root()
|
||||
filename = request.query.get("filename")
|
||||
subfolder = request.query.get("subfolder", "")
|
||||
@@ -661,68 +713,73 @@ async def get_metadata(request: web.Request) -> web.Response:
|
||||
{"ok": False, "error": "File not found"}, status=404
|
||||
)
|
||||
|
||||
def _extract(path: Path) -> Dict[str, Any]:
|
||||
try:
|
||||
return extract_generation_params_from_png(path) or {}
|
||||
except Exception as e:
|
||||
print(f"[Majoor.AssetsManager] metadata parsing error for {path.name}: {e}")
|
||||
return {}
|
||||
def _extract_generation(path: Path) -> Dict[str, Any]:
|
||||
def _extract_one(p: Path) -> Dict[str, Any]:
|
||||
try:
|
||||
return extract_generation_params_from_png(p) or {}
|
||||
except Exception as e:
|
||||
print(f"[Majoor.AssetsManager] metadata parsing error for {p.name}: {e}")
|
||||
return {}
|
||||
|
||||
ext_lower = target.suffix.lower()
|
||||
params: Dict[str, Any] = _extract(target)
|
||||
|
||||
def _has_wf(p: Dict[str, Any]) -> bool:
|
||||
return bool(
|
||||
p.get("has_workflow")
|
||||
or p.get("workflow")
|
||||
or p.get("prompt")
|
||||
or p.get("positive_prompt")
|
||||
or p.get("negative_prompt")
|
||||
or p.get("sampler_name")
|
||||
or p.get("model")
|
||||
or p.get("loras")
|
||||
)
|
||||
|
||||
if ext_lower in {".mp4", ".mov", ".m4v", ".webm", ".mkv"} and not _has_wf(params):
|
||||
stem = target.stem
|
||||
candidates = []
|
||||
try:
|
||||
for entry in target.parent.iterdir():
|
||||
if not entry.is_file():
|
||||
continue
|
||||
if entry.suffix.lower() != ".png":
|
||||
continue
|
||||
if entry.stem == stem or entry.stem.startswith(f"{stem}_") or entry.stem.startswith(f"{stem}-"):
|
||||
candidates.append(entry)
|
||||
except Exception:
|
||||
candidates = []
|
||||
|
||||
if candidates:
|
||||
candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
for png in candidates:
|
||||
sib_params = _extract(png)
|
||||
if _has_wf(sib_params):
|
||||
params = sib_params
|
||||
params["generation_source"] = "sibling_png"
|
||||
break
|
||||
|
||||
if isinstance(params, dict):
|
||||
if "has_workflow" not in params:
|
||||
params["has_workflow"] = bool(
|
||||
params.get("has_workflow")
|
||||
or params.get("workflow")
|
||||
or params.get("prompt")
|
||||
or params.get("positive_prompt")
|
||||
or params.get("negative_prompt")
|
||||
or params.get("sampler_name")
|
||||
or params.get("model")
|
||||
or params.get("loras")
|
||||
def _has_wf(p: Dict[str, Any]) -> bool:
|
||||
return bool(
|
||||
p.get("has_workflow")
|
||||
or p.get("workflow")
|
||||
or p.get("prompt")
|
||||
or p.get("positive_prompt")
|
||||
or p.get("negative_prompt")
|
||||
or p.get("sampler_name")
|
||||
or p.get("model")
|
||||
or p.get("loras")
|
||||
)
|
||||
|
||||
ext_lower = path.suffix.lower()
|
||||
params: Dict[str, Any] = _extract_one(path)
|
||||
|
||||
if ext_lower in {".mp4", ".mov", ".m4v", ".webm", ".mkv"} and not _has_wf(params):
|
||||
stem = path.stem
|
||||
candidates = []
|
||||
try:
|
||||
for entry in path.parent.iterdir():
|
||||
if not entry.is_file():
|
||||
continue
|
||||
if entry.suffix.lower() != ".png":
|
||||
continue
|
||||
if (
|
||||
entry.stem == stem
|
||||
or entry.stem.startswith(f"{stem}_")
|
||||
or entry.stem.startswith(f"{stem}-")
|
||||
):
|
||||
candidates.append(entry)
|
||||
except Exception:
|
||||
candidates = []
|
||||
|
||||
if candidates:
|
||||
candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
for png in candidates:
|
||||
sib_params = _extract_one(png)
|
||||
if _has_wf(sib_params):
|
||||
params = sib_params
|
||||
params["generation_source"] = "sibling_png"
|
||||
break
|
||||
|
||||
if isinstance(params, dict) and "has_workflow" not in params:
|
||||
params["has_workflow"] = _has_wf(params)
|
||||
|
||||
return params
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
params = await loop.run_in_executor(None, _extract_generation, target)
|
||||
|
||||
kind = classify_ext(filename.lower())
|
||||
rating, tags = _rating_tags_with_fallback(target, kind)
|
||||
|
||||
return web.json_response({"ok": True, "generation": params, "rating": rating, "tags": tags})
|
||||
try:
|
||||
return web.json_response(
|
||||
{"ok": True, "generation": params, "rating": rating, "tags": tags}
|
||||
)
|
||||
except ConnectionResetError:
|
||||
return web.Response(status=499)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/mjr/filemanager/capabilities")
|
||||
|
||||
Reference in New Issue
Block a user