diff --git a/html_generator.py b/html_generator.py index 2cbc147..8e1be2b 100644 --- a/html_generator.py +++ b/html_generator.py @@ -22,6 +22,7 @@ def get_html_template(title, manifest_data, node_id, standalone=False): "logic_state.js", # Global variables "logic_utils.js", # API & Helper functions "logic_ratings.js", # Pairwise review: pure logic (normalisers, Bradley-Terry, pairing, scoreboard) + "logic_dragdrop.js", # Revise-modal image drag/send helpers (issue #22; pure, node-testable) "logic_ui.js", # DOM Creation, Modals, JSON Bars "logic_upscale_modal.js", # Upscale Modal (preset management, async upscale) "logic_virtual.js", # The Virtual Scroll Engine & Viewport diff --git a/resources/logic_dragdrop.js b/resources/logic_dragdrop.js new file mode 100644 index 0000000..f15ea37 --- /dev/null +++ b/resources/logic_dragdrop.js @@ -0,0 +1,63 @@ +/** + * Drag-and-drop / "Send to Load Image" helpers for the Revise modal image (issue #22). + * Pure functions only (no DOM): loaded as a plain script inlined into the dashboard HTML and, via the + * guarded export at the bottom, by node (tests/js/dragdrop.test.js). Top-level names are shared with every + * other logic_*.js (ONE inlined script), so everything here is prefixed `uscg`/`USCG_`. + */ + +// false = drag/send REFERENCE the image in the benchmark folder (`subfolder/file [output]`, no copy in input/). +// true = additionally attach the image bytes as a File (drag) / upload to input/ (button). +const USCG_DRAG_UPLOAD = false; + +const USCG_DRAG_MIME = { webp: 'image/webp', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg' }; + +// Parse a manifest item's `file` (a /view?filename=..&type=..&subfolder=.. URL) into what the drop +// handlers need. Returns null for items that are not draggable images (videos, workflow cards, odd URLs). +function uscgImageRef(d, baseHref) { + if (!d || !d.file || d.media_type === 'video' || d.model_type === 'workflow') return null; + var u; + try { u = new URL(d.file, baseHref || (typeof document !== 'undefined' ? document.baseURI : 'http://localhost/')); } + catch (e) { return null; } + var filename = u.searchParams.get('filename'); + if (!filename) return null; + var m = /\.([A-Za-z0-9]+)$/.exec(filename); + var mime = m ? USCG_DRAG_MIME[m[1].toLowerCase()] : null; + if (!mime) return null; + return { + absUrl: u.href, + filename: filename, + subfolder: u.searchParams.get('subfolder') || '', + type: u.searchParams.get('type') || 'output', + mime: mime + }; +} + +// The JSON ComfyUI's own Assets panel puts under `application/x-comfy-asset-info` (documented fields only). +function uscgImageAssetInfo(ref) { + return { filename: ref.filename, subfolder: ref.subfolder, type: ref.type, display_name: ref.filename }; +} + +// Widget value LoadImage accepts for a file that lives in output/ (annotated path, no copy). +function uscgAnnotatedPath(ref) { + return (ref.subfolder ? ref.subfolder + '/' : '') + ref.filename + ' [' + ref.type + ']'; +} + +// Fill a DataTransfer during `dragstart`. `file` (a File) is attached only when given. +function uscgFillDragData(dt, ref, file) { + dt.setData('application/x-comfy-asset-info', JSON.stringify(uscgImageAssetInfo(ref))); + dt.setData('text/uri-list', ref.absUrl); + dt.setData('text/plain', ref.absUrl); + dt.setData('DownloadURL', ref.mime + ':' + ref.filename + ':' + ref.absUrl); + if (file && dt.items && typeof dt.items.add === 'function') { + try { dt.items.add(file); } catch (e) { /* protected mode / unsupported: URL carriers still work */ } + } + try { dt.effectAllowed = 'copy'; } catch (e) { /* ignore */ } + return dt; +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + USCG_DRAG_UPLOAD: USCG_DRAG_UPLOAD, uscgImageRef: uscgImageRef, uscgImageAssetInfo: uscgImageAssetInfo, + uscgAnnotatedPath: uscgAnnotatedPath, uscgFillDragData: uscgFillDragData + }; +} diff --git a/resources/logic_standalone.js b/resources/logic_standalone.js index 373dc1f..dc7e98c 100644 --- a/resources/logic_standalone.js +++ b/resources/logic_standalone.js @@ -71,6 +71,12 @@ if (USCG_STANDALONE) { var openBtn = document.getElementById('open-standalone-btn'); if (openBtn) openBtn.style.display = 'none'; + var sendBtn = document.getElementById('modal-send-loadimage-btn'); + if (sendBtn) { + sendBtn.textContent = '\u2B07 Download image'; + sendBtn.title = 'Save this image (dragging it to the desktop also works)'; + } + var pack = document.getElementById('pack-workflow-checkbox'); if (pack) { pack.checked = false; diff --git a/resources/logic_ui.js b/resources/logic_ui.js index 2d172f8..61bed06 100644 --- a/resources/logic_ui.js +++ b/resources/logic_ui.js @@ -1423,12 +1423,95 @@ function createCard(d) { return card; } +// ---- Revise-modal image: drag onto a Load Image node / canvas, or "Send to Load Image" (issue #22) ---- +// The drag carries ComfyUI's own asset-info JSON + a same-origin URL (what its Assets panel sends), so a node +// drop references the file in output/ and a canvas drop creates a Load Image node. Helpers: logic_dragdrop.js. +function _uscgModalRef() { + const d = activeData.find(x => x.id === window.currentModalId); + return d ? uscgImageRef(d) : null; +} + +async function _uscgPrefetchDragFile(ref, id) { + if (!USCG_DRAG_UPLOAD || !ref) return; + if (window._uscgDragFile && window._uscgDragFile.id === id) return; + try { + const blob = await (await fetch(ref.absUrl)).blob(); + if (window.currentModalId === id) window._uscgDragFile = { id: id, file: new File([blob], ref.filename, { type: blob.type || ref.mime }) }; + } catch (e) { /* no File: the URL carriers still work */ } +} + +function _uscgInstallModalDrag() { + if (window._uscgDragInstalled) return; + const img = document.getElementById('m-img'); + if (!img) return; + window._uscgDragInstalled = true; + img.addEventListener('dragstart', function (e) { + const ref = _uscgModalRef(); + if (!ref || !e.dataTransfer) { e.preventDefault(); return; } + const f = window._uscgDragFile && window._uscgDragFile.id === window.currentModalId ? window._uscgDragFile.file : null; + uscgFillDragData(e.dataTransfer, ref, USCG_DRAG_UPLOAD ? f : null); + }); +} + +function _uscgSyncModalDrag(d) { + _uscgInstallModalDrag(); + const ref = uscgImageRef(d); + const img = document.getElementById('m-img'); + if (img) img.draggable = !!ref; + const btn = document.getElementById('modal-send-loadimage-btn'); + if (btn) { + btn.style.display = ref ? '' : 'none'; + btn.textContent = (typeof USCG_STANDALONE !== 'undefined' && USCG_STANDALONE) + ? '\u2B07 Download image' : '\uD83D\uDDBC Send to Load Image'; + } + window._uscgDragFile = null; + _uscgPrefetchDragFile(ref, d.id); +} + +function _uscgAskParentLoadImage(ref, upload) { + return new Promise(function (resolve) { + if (!window.parent || window.parent === window) { resolve(null); return; } + var done = false; + var onMsg = function (ev) { + if (!ev.data || ev.data.type !== 'uscg_send_to_load_image_result') return; + done = true; + window.removeEventListener('message', onMsg); + resolve({ ok: !!ev.data.ok, message: ev.data.message || '' }); + }; + window.addEventListener('message', onMsg); + window.parent.postMessage({ + type: 'uscg_send_to_load_image', url: ref.absUrl, filename: ref.filename, subfolder: ref.subfolder, + imageType: ref.type, upload: !!upload, node_id: TARGET_NODE_ID + }, '*'); + setTimeout(function () { + if (done) return; + window.removeEventListener('message', onMsg); + resolve(null); + }, 8000); + }); +} + +async function sendModalToLoadImage() { + const ref = _uscgModalRef(); + if (!ref) { _uscgToast('This item is not an image.', false); return; } + if (typeof USCG_STANDALONE !== 'undefined' && USCG_STANDALONE) { + const a = document.createElement('a'); + a.href = ref.absUrl; a.download = ref.filename; + document.body.appendChild(a); a.click(); a.remove(); + return; + } + const res = await _uscgAskParentLoadImage(ref, USCG_DRAG_UPLOAD); + if (res) { _uscgToast(res.message || (res.ok ? 'Sent to Load Image.' : 'Could not send.'), res.ok); return; } + _uscgToast('The ComfyUI tab did not answer. Drag the image onto a Load Image node instead.', false); +} + // Open Revision Modal function openM(id) { window.currentModalId = id; const d = activeData.find(x => x.id === id); if (!d) return; document.getElementById('m-img').src = d.file; + _uscgSyncModalDrag(d); const previewNote = document.getElementById('m-preview-note'); if (previewNote) { if (d.preview) { diff --git a/resources/template.html b/resources/template.html index b1bb63c..bea34d0 100644 --- a/resources/template.html +++ b/resources/template.html @@ -858,13 +858,18 @@ ← → Space navigate  |  1-9 favorite