diff --git a/__init__.py b/__init__.py index eab1cbd..b8e3c64 100644 --- a/__init__.py +++ b/__init__.py @@ -343,6 +343,7 @@ try: LOOP_NODE_DISPLAY_NAME_MAPPINGS, accumulator_delete_entries, accumulator_list_entries, + accumulator_move_entry, ) from .prompt_builder import ( build_camera_config_json, @@ -418,6 +419,7 @@ except ImportError: LOOP_NODE_DISPLAY_NAME_MAPPINGS, accumulator_delete_entries, accumulator_list_entries, + accumulator_move_entry, ) from prompt_builder import ( build_camera_config_json, @@ -524,6 +526,20 @@ if PromptServer is not None and web is not None: except Exception as exc: return web.json_response({"error": str(exc)}, status=400) + @PromptServer.instance.routes.post("/sxcp/accumulator/move") + async def sxcp_accumulator_move(request): + try: + payload = await request.json() + result = accumulator_move_entry( + store_key=str(payload.get("store_key") or ""), + entry_id=str(payload.get("entry_id") or ""), + index=int(payload.get("index") or 0), + direction=str(payload.get("direction") or "up"), + ) + return web.json_response(result) + except Exception as exc: + return web.json_response({"error": str(exc)}, status=400) + class SxCPPromptBuilder: @classmethod diff --git a/loop_nodes.py b/loop_nodes.py index 88d69e5..a17d32e 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -283,6 +283,57 @@ def accumulator_delete_entries( return result +def accumulator_move_entry( + store_key: str, + entry_id: str = "", + index: int = 0, + direction: str = "up", +) -> dict[str, Any]: + key = str(store_key or "").strip() + if not key: + raise ValueError("store_key is required for accumulator preview actions") + store = _ACCUMULATOR_STORES.setdefault(key, []) + if not store: + result = accumulator_list_entries(key) + result["moved"] = False + return result + zero_index = -1 + entry_id = str(entry_id or "").strip() + if entry_id: + for current_index, entry in enumerate(store): + if str(entry.get("id") or "") == entry_id: + zero_index = current_index + break + elif int(index) > 0: + candidate = int(index) - 1 + if candidate < len(store): + zero_index = candidate + else: + raise ValueError("entry_id or 1-based index is required") + if zero_index < 0: + result = accumulator_list_entries(key) + result["moved"] = False + return result + direction = str(direction or "up").strip().lower() + if direction == "top": + target_index = 0 + elif direction == "bottom": + target_index = len(store) - 1 + elif direction == "down": + target_index = min(len(store) - 1, zero_index + 1) + else: + target_index = max(0, zero_index - 1) + moved = target_index != zero_index + if moved: + entry = store.pop(zero_index) + store.insert(target_index, entry) + result = accumulator_list_entries(key) + result["moved"] = moved + result["from_index"] = zero_index + 1 + result["to_index"] = target_index + 1 + return result + + def _require_image_saving() -> None: if folder_paths is None or np is None or Image is None: raise RuntimeError("Image preview/save helpers require ComfyUI image dependencies.") diff --git a/web/accumulator_preview.js b/web/accumulator_preview.js index 4496621..ca0afed 100644 --- a/web/accumulator_preview.js +++ b/web/accumulator_preview.js @@ -154,6 +154,31 @@ async function deleteSelected(node) { } } +async function moveSelected(node, direction) { + const key = storeKey(node); + if (!key) { + alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first."); + return; + } + const entry = selectedEntry(node); + if (!entry) { + alert("No accumulator entry selected."); + return; + } + try { + const data = await postJson("/sxcp/accumulator/move", { + store_key: key, + entry_id: entry.id || "", + index: entry.id ? 0 : entry.index, + direction, + }); + setEntries(node, data.entries || [], `${data.status || ""}; moved=${data.moved ? "yes" : "no"}; rerun preview to refresh images`); + } catch (err) { + console.error(`[${EXTENSION}] move failed`, err); + alert(`Move failed: ${err}`); + } +} + async function clearStore(node) { const key = storeKey(node); if (!key) { @@ -183,6 +208,18 @@ function setupNode(node) { node._sxcpAccumulatorStatusWidget = node.addWidget("text", "accumulator_status", "no accumulator data", () => {}); node._sxcpAccumulatorStatusWidget.serialize = false; } + if (!node._sxcpMoveTopButton) { + node._sxcpMoveTopButton = node.addWidget("button", "Move Selected Top", null, () => moveSelected(node, "top")); + } + if (!node._sxcpMoveUpButton) { + node._sxcpMoveUpButton = node.addWidget("button", "Move Selected Up", null, () => moveSelected(node, "up")); + } + if (!node._sxcpMoveDownButton) { + node._sxcpMoveDownButton = node.addWidget("button", "Move Selected Down", null, () => moveSelected(node, "down")); + } + if (!node._sxcpMoveBottomButton) { + node._sxcpMoveBottomButton = node.addWidget("button", "Move Selected Bottom", null, () => moveSelected(node, "bottom")); + } if (!node._sxcpDeleteSelectedButton) { node._sxcpDeleteSelectedButton = node.addWidget("button", "Delete Selected Entry", null, () => deleteSelected(node)); }