diff --git a/.gitignore b/.gitignore index a135311..d5ea75e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ preset_colors/ preset_models/ presets/ +presets_workflow prompts/ rayko_lora_data/ styles/ diff --git a/__init__.py b/__init__.py index 57e31ed..5fbcf42 100644 --- a/__init__.py +++ b/__init__.py @@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'} REQUIRED_MODULES: Set[str] = set() PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio" -__version__ = "0.51.4" +__version__ = "0.52.0" def load_modules(): print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}") diff --git a/package.json b/package.json index ef19a17..d509c0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.51.4", + "version": "0.52.0", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "Apache License 2.0", diff --git a/pyproject.toml b/pyproject.toml index 8723b57..1405b77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.51.4" +version = "0.52.0" license = { file = "LICENSE" } dependencies = ["torch", "opencv-python", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"] diff --git a/settings_manager_api.py b/settings_manager_api.py index 357ebd0..0ab4968 100644 --- a/settings_manager_api.py +++ b/settings_manager_api.py @@ -15,6 +15,7 @@ import os import sys +import json import shutil import datetime import logging @@ -28,20 +29,27 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) COMFYUI_ROOT = os.path.dirname(os.path.dirname(CURRENT_DIR)) SETTINGS_FILE = os.path.join(COMFYUI_ROOT, "user", "default", "comfy.settings.json") BACKUP_DIR = os.path.join(os.path.expanduser("~"), "Documents", "ComfyUI_Settings_Backups") +PRESETS_DIR = os.path.join(CURRENT_DIR, "presets_workflow") os.makedirs(BACKUP_DIR, exist_ok=True) +os.makedirs(PRESETS_DIR, exist_ok=True) logger.info("🦊 Settings Manager initialized") +logger.info(f"🦊 Presets folder: {PRESETS_DIR}") + + +# -------------------- Settings Backups -------------------- @PromptServer.instance.routes.get("/rayko_settings_manager/ping") async def ping(request): return web.json_response({"status": "ok"}) + @PromptServer.instance.routes.post("/rayko_settings_manager/save") async def save_settings(request): try: if not os.path.exists(SETTINGS_FILE): return web.json_response({"error": "Settings file not found"}, status=404) - + try: data = await request.json() custom_name = data.get('backup_name', '').strip() @@ -62,60 +70,64 @@ async def save_settings(request): target_folder = os.path.join(BACKUP_DIR, folder_name) os.makedirs(target_folder, exist_ok=True) shutil.copy2(SETTINGS_FILE, target_folder) - + return web.json_response({"success": True, "folder": folder_name}) except Exception as e: return web.json_response({"error": str(e)}, status=500) + @PromptServer.instance.routes.get("/rayko_settings_manager/list") async def list_settings(request): try: if not os.path.exists(BACKUP_DIR): return web.json_response({"success": True, "backups": []}) - + backups = [d for d in os.listdir(BACKUP_DIR) if os.path.isdir(os.path.join(BACKUP_DIR, d))] backups.sort(reverse=True) return web.json_response({"success": True, "backups": backups}) except Exception as e: return web.json_response({"error": str(e)}, status=500) + @PromptServer.instance.routes.post("/rayko_settings_manager/restore") async def restore_settings(request): try: data = await request.json() backup_name = data.get("backup_name") - + if not backup_name: return web.json_response({"error": "Backup name not specified"}, status=400) - + backup_file = os.path.join(BACKUP_DIR, backup_name, "comfy.settings.json") - + if not os.path.exists(backup_file): return web.json_response({"error": "Backup file not found"}, status=404) - + shutil.copy2(backup_file, SETTINGS_FILE) return web.json_response({"success": True}) except Exception as e: return web.json_response({"error": str(e)}, status=500) + @PromptServer.instance.routes.delete("/rayko_settings_manager/delete/{backup_name}") async def delete_backup(request): try: backup_name = request.match_info.get('backup_name') - + if not backup_name: return web.json_response({"error": "Backup name not specified"}, status=400) - + backup_path = os.path.join(BACKUP_DIR, backup_name) - + if not os.path.exists(backup_path): return web.json_response({"error": "Backup not found"}, status=404) - + shutil.rmtree(backup_path) return web.json_response({"success": True}) except Exception as e: return web.json_response({"error": str(e)}, status=500) + @PromptServer.instance.routes.post("/rayko_settings_manager/restart") async def restart_server(request): try: @@ -124,5 +136,87 @@ async def restart_server(request): except Exception as e: return web.json_response({"error": str(e)}, status=500) + +# -------------------- Workflow Presets -------------------- + +def _sanitize_name(name: str) -> str: + return re.sub(r'[<>:"/\\|?*]', '_', (name or "").strip()).rstrip('. ') + + +@PromptServer.instance.routes.get("/rayko_settings_manager/presets/list") +async def list_presets(request): + try: + presets = [] + if os.path.exists(PRESETS_DIR): + for f in sorted(os.listdir(PRESETS_DIR)): + if not f.endswith(".json"): + continue + try: + with open(os.path.join(PRESETS_DIR, f), "r", encoding="utf-8") as fp: + data = json.load(fp) + presets.append({ + "name": f[:-5], + "count": len(data.get("workflows", [])), + }) + except Exception as e: + logger.warning(f"Failed to read preset {f}: {e}") + return web.json_response({"success": True, "presets": presets}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.get("/rayko_settings_manager/presets/get/{name}") +async def get_preset(request): + try: + name = _sanitize_name(request.match_info.get("name") or "") + if not name: + return web.json_response({"error": "Name required"}, status=400) + path = os.path.join(PRESETS_DIR, f"{name}.json") + if not os.path.exists(path): + return web.json_response({"error": "Not found"}, status=404) + with open(path, "r", encoding="utf-8") as f: + return web.json_response(json.load(f)) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.post("/rayko_settings_manager/presets/save") +async def save_preset(request): + try: + data = await request.json() + name = _sanitize_name(data.get("name", "")) + workflows = data.get("workflows", []) + if not name: + return web.json_response({"error": "Name required"}, status=400) + if not workflows: + return web.json_response({"error": "No workflows to save"}, status=400) + + payload = { + "created": datetime.datetime.now().isoformat(timespec="seconds"), + "workflows": workflows, + } + path = os.path.join(PRESETS_DIR, f"{name}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + return web.json_response({"success": True, "name": name, "count": len(workflows)}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@PromptServer.instance.routes.delete("/rayko_settings_manager/presets/delete/{name}") +async def delete_preset(request): + try: + name = _sanitize_name(request.match_info.get("name") or "") + if not name: + return web.json_response({"error": "Name required"}, status=400) + path = os.path.join(PRESETS_DIR, f"{name}.json") + if not os.path.exists(path): + return web.json_response({"error": "Not found"}, status=404) + os.remove(path) + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} \ No newline at end of file diff --git a/web/settings_manager.js b/web/settings_manager.js index 4a43241..a421eb6 100644 --- a/web/settings_manager.js +++ b/web/settings_manager.js @@ -1,489 +1,518 @@ import { app } from "../../../scripts/app.js"; -let topbarInterval = null; +console.log("🦊 Settings Manager JS v4 (preset save logic) loaded at", new Date().toLocaleTimeString()); + const STORAGE_KEY = "RaykoStudio.ShowIcon"; +const ACTIVE_PRESET_KEY = "RaykoStudio.ActivePreset"; + +let _activePresetName = sessionStorage.getItem(ACTIVE_PRESET_KEY) || null; app.registerExtension({ name: "RaykoStudio.SettingsManager", - + async setup() { - // Одна настройка без явной category — ComfyUI возьмет раздел из id (часть до точки) + injectStyles(); + app.ui.settings.addSetting({ - id: "RaykoStudio.SettingsManager", + id: "RaykoStudio.SettingsManager.Open", name: "Settings Manager", - type: () => { - const container = document.createElement("div"); - container.id = "sm-settings-container"; - container.style.display = "flex"; - container.style.flexDirection = "column"; - container.style.gap = "4px"; - container.style.margin = "0"; - container.style.padding = "0"; - - // --- Строка с тогглом --- - const toggleRow = document.createElement("label"); - toggleRow.style.display = "flex"; - toggleRow.style.alignItems = "center"; - toggleRow.style.gap = "10px"; - toggleRow.style.cursor = "pointer"; - toggleRow.style.fontSize = "14px"; - toggleRow.style.lineHeight = "1.2"; - - const toggleInput = document.createElement("input"); - toggleInput.type = "checkbox"; - toggleInput.style.cursor = "pointer"; - toggleInput.style.width = "16px"; - toggleInput.style.height = "16px"; - toggleInput.style.margin = "0"; - - const isEnabled = localStorage.getItem(STORAGE_KEY) === "true"; - toggleInput.checked = isEnabled; - - toggleInput.onchange = (e) => { - localStorage.setItem(STORAGE_KEY, e.target.checked ? "true" : "false"); - updateTopbarIcon(e.target.checked); - }; - - const toggleText = document.createElement("span"); - toggleText.textContent = "Show Icon On Menu"; - toggleText.style.color = "var(--fg-color)"; - - toggleRow.appendChild(toggleInput); - toggleRow.appendChild(toggleText); - - // --- Кнопка открытия менеджера --- - const btn = document.createElement("button"); - btn.className = "sm-btn sm-btn-success"; - btn.textContent = "Open Settings Manager"; - btn.style.width = "auto"; - btn.style.alignSelf = "flex-start"; - btn.style.padding = "6px 12px"; - btn.style.fontSize = "13px"; - btn.style.marginTop = "4px"; - btn.onclick = () => openSettingsManagerModal(); - - container.appendChild(toggleRow); - container.appendChild(btn); - - return container; - } + type: () => buildSettingsEntry(), }); - // Инициализация при загрузке - setTimeout(() => { - const initialVal = localStorage.getItem(STORAGE_KEY) === "true"; - updateTopbarIcon(initialVal); - }, 1000); + if (localStorage.getItem(STORAGE_KEY) === "true") { + waitForActionBar((bar) => injectTopbarButton(bar)); + } + + _startTopbarWatchdog(); }, }); -// --- Управление иконкой в верхнем меню --- +function buildSettingsEntry() { + const container = document.createElement("div"); + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "8px"; -function updateTopbarIcon(show) { - if (show) { - startTopbarInjection(); - } else { - stopTopbarInjection(); - const existingBtn = document.getElementById("sm-topbar-btn"); - if (existingBtn) { - existingBtn.remove(); - console.log("🦊 Settings Manager: Иконка скрыта."); + const toggleRow = document.createElement("label"); + toggleRow.style.display = "flex"; + toggleRow.style.alignItems = "center"; + toggleRow.style.gap = "10px"; + toggleRow.style.cursor = "pointer"; + + const toggle = document.createElement("input"); + toggle.type = "checkbox"; + toggle.checked = localStorage.getItem(STORAGE_KEY) === "true"; + toggle.onchange = () => { + localStorage.setItem(STORAGE_KEY, toggle.checked ? "true" : "false"); + const existing = document.getElementById("sm-topbar-block"); + if (toggle.checked) { + waitForActionBar((bar) => injectTopbarButton(bar)); + } else if (existing) { + existing.remove(); } - } + }; + + const toggleLabel = document.createElement("span"); + toggleLabel.textContent = "Show icon in top menu"; + toggleLabel.style.color = "var(--fg-color)"; + + toggleRow.appendChild(toggle); + toggleRow.appendChild(toggleLabel); + + container.appendChild(toggleRow); + return container; } -function startTopbarInjection() { - if (topbarInterval) return; - - console.log("🦊 Settings Manager: Начинаем поиск верхнего меню..."); - let attempts = 0; - const maxAttempts = 20; +function waitForActionBar(callback) { + const existing = document.querySelector(".actionbar-container"); + if (existing) return callback(existing); - topbarInterval = setInterval(() => { - attempts++; - const actionBar = document.querySelector('.actionbar-container'); - - if (actionBar) { - if (document.getElementById("sm-topbar-btn")) { - clearInterval(topbarInterval); - topbarInterval = null; - return; - } + const observer = new MutationObserver(() => { + const bar = document.querySelector(".actionbar-container"); + if (bar) { + observer.disconnect(); + callback(bar); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + setTimeout(() => observer.disconnect(), 30000); +} - const topBtn = document.createElement("button"); - topBtn.id = "sm-topbar-btn"; - topBtn.className = "comfyui-button"; - topBtn.title = "Settings Manager"; - topBtn.innerHTML = `🖥️`; - topBtn.onclick = () => openSettingsManagerModal(); +function injectTopbarButton(actionBar) { + if (localStorage.getItem(STORAGE_KEY) !== "true") return; + if (document.getElementById("sm-topbar-block")) return; + if (!actionBar || !document.body.contains(actionBar)) return; - topBtn.style.width = "38px"; - topBtn.style.height = "100%"; - topBtn.style.minHeight = "32px"; - topBtn.style.maxHeight = "40px"; - topBtn.style.padding = "0"; - topBtn.style.margin = "0 5px"; - topBtn.style.display = "inline-flex"; - topBtn.style.alignItems = "center"; - topBtn.style.justifyContent = "center"; - topBtn.style.cursor = "pointer"; - topBtn.style.background = "var(--comfy-input-bg)"; - topBtn.style.color = "var(--fg-color)"; - topBtn.style.border = "1px solid var(--border-color)"; - topBtn.style.borderRadius = "8px"; - topBtn.style.transition = "background 0.2s"; - topBtn.style.boxSizing = "border-box"; + const block = document.createElement("div"); + block.id = "sm-topbar-block"; + block.className = "sm-topbar-block"; - topBtn.onmouseenter = () => { topBtn.style.background = "var(--comfy-menu-secondary-bg)"; }; - topBtn.onmouseleave = () => { topBtn.style.background = "var(--comfy-input-bg)"; }; + const btn = document.createElement("button"); + btn.className = "sm-topbar-btn"; + btn.title = "Settings Manager"; + btn.innerHTML = `🖥️`; + btn.onclick = () => openSettingsManagerModal(); + block.appendChild(btn); - const runContainer = actionBar.querySelector('.flex.h-full.items-center'); - if (runContainer) { - actionBar.insertBefore(topBtn, runContainer); - } else { - actionBar.appendChild(topBtn); - } - - console.log("🦊 Settings Manager: Иконка добавлена в верхнее меню."); - clearInterval(topbarInterval); - topbarInterval = null; - } else { - if (attempts >= maxAttempts) { - console.error("🦊 Settings Manager: Не удалось найти .actionbar-container."); - clearInterval(topbarInterval); - topbarInterval = null; - } + const closeAllBtn = document.createElement("button"); + closeAllBtn.className = "sm-topbar-btn"; + closeAllBtn.title = "Close All Workflows"; + closeAllBtn.innerHTML = `🗑️`; + closeAllBtn.onclick = () => closeAllWorkflowsWithConfirm(); + block.appendChild(closeAllBtn); + + const runContainer = actionBar.querySelector(".flex.h-full.items-center"); + if (runContainer) { + actionBar.insertBefore(block, runContainer); + } else { + actionBar.appendChild(block); + } + + console.log("🦊 Settings Manager: кнопки добавлены в панель RUN."); +} + +function _startTopbarWatchdog() { + if (window.__smTopbarWatchdog) return; + window.__smTopbarWatchdog = true; + + setInterval(() => { + if (localStorage.getItem(STORAGE_KEY) !== "true") return; + + const block = document.getElementById("sm-topbar-block"); + const bar = document.querySelector(".actionbar-container"); + + if (!bar) return; + + if (!block) { + injectTopbarButton(bar); + return; + } + + if (!bar.contains(block)) { + try { block.remove(); } catch (e) { } + injectTopbarButton(bar); + return; + } + + if (!block.classList.contains("sm-topbar-block")) { + block.classList.add("sm-topbar-block"); } }, 500); } -function stopTopbarInjection() { - if (topbarInterval) { - clearInterval(topbarInterval); - topbarInterval = null; +function setActivePreset(name) { + _activePresetName = name; + if (name) { + sessionStorage.setItem(ACTIVE_PRESET_KEY, name); + } else { + sessionStorage.removeItem(ACTIVE_PRESET_KEY); + } + updatePresetUI(); +} + +async function saveCurrentStateToActivePreset() { + if (!_activePresetName) return; + const workflows = getOpenWorkflowsData(); + if (workflows.length === 0) throw new Error("No valid workflows to save."); + + const res = await fetch("/rayko_settings_manager/presets/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: _activePresetName, workflows }), + }); + const data = await res.json(); + if (!data.success) throw new Error(data.error || "Save failed"); +} + +async function closeAllWorkflowsWithConfirm() { + const wfStore = app.extensionManager?.workflow; + if (!wfStore || !wfStore.openWorkflows || wfStore.openWorkflows.length === 0) { + if (_activePresetName) setActivePreset(null); + return; + } + + if (_activePresetName) { + const action = await smConfirmSavePreset(_activePresetName); + if (action === "cancel") return; + if (action === "save") { + try { + await saveCurrentStateToActivePreset(); + } catch (e) { + await smAlert("Save Failed", e.message, true); + return; + } + } + } else { + const confirmed = await smConfirm({ + title: "Close All Workflows", + message: `You have ${wfStore.openWorkflows.length} workflow(s) open. Any unsaved changes will be lost. Are you sure?`, + okText: "Close All", + danger: true + }); + if (!confirmed) return; + } + + await performCloseAll(); + setActivePreset(null); +} + +async function performCloseAll() { + const wfStore = app.extensionManager?.workflow; + if (!wfStore) return; + + const activeWf = wfStore.activeWorkflow; + const allWfs = [...(wfStore.openWorkflows || [])]; + + for (const wf of allWfs) { + if (wf === activeWf) continue; + try { + if (typeof wfStore.closeWorkflow === "function") { + await wfStore.closeWorkflow(wf); + } else if (typeof wf.unload === "function") { + await wf.unload(); + } + } catch (e) { + console.warn("[Rayko] Failed to close inactive workflow", e); + } + } + + if (activeWf) { + try { + if (typeof activeWf.load === "function") { + await activeWf.load(); + } + const emptyState = { + nodes: [], links: [], groups: [], config: {}, extra: {}, version: 0.4 + }; + await app.loadGraphData(emptyState, true, true, activeWf); + if (typeof wfStore.renameWorkflow === "function") { + await wfStore.renameWorkflow(activeWf, "Unsaved Workflow"); + } + } catch (e) { + console.warn("[Rayko] Failed to clean active workflow", e); + try { app.graph.clear(); } catch (e) {} + } + } + + if (Array.isArray(wfStore.openWorkflowPaths) && activeWf?.path) { + wfStore.openWorkflowPaths = [activeWf.path]; } } -// --- Модальное окно и интерфейс менеджера --- - function openSettingsManagerModal() { + if (document.getElementById("sm-modal-overlay")) return; + const overlay = document.createElement("div"); + overlay.id = "sm-modal-overlay"; overlay.className = "sm-modal-overlay"; - + overlay.onclick = (e) => { + if (e.target === overlay) overlay.remove(); + }; + const modal = document.createElement("div"); modal.className = "sm-modal-content"; - + const closeBtn = document.createElement("button"); closeBtn.className = "sm-btn sm-btn-danger sm-btn-small"; closeBtn.textContent = "✕ Close"; closeBtn.style.width = "100%"; closeBtn.style.marginTop = "10px"; - closeBtn.onclick = () => document.body.removeChild(overlay); - + closeBtn.onclick = () => overlay.remove(); + renderSettingsPanel(modal); modal.appendChild(closeBtn); overlay.appendChild(modal); - - overlay.onclick = (e) => { - if (e.target === overlay) document.body.removeChild(overlay); - }; - document.body.appendChild(overlay); + + updatePresetUI(); } function renderSettingsPanel(container) { - container.innerHTML = ''; - - const style = document.createElement('style'); - style.textContent = ` - .sm-modal-overlay { - position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.6); - display: flex; justify-content: center; align-items: center; - z-index: 10000; - } - .sm-modal-content { - background: var(--comfy-menu-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 20px; - width: 90%; max-width: 450px; - max-height: 90vh; overflow-y: auto; - box-shadow: 0 4px 6px rgba(0,0,0,0.3); - position: relative; - } - .sm-container { display: flex; flex-direction: column; gap: 20px; min-height: 300px; } - .sm-section { background-color: var(--comfy-input-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 15px; } - .sm-title { font-size: 16px; font-weight: bold; margin-bottom: 10px; color: var(--fg-color); } - .sm-description { font-size: 12px; color: var(--descrip-text); margin-bottom: 15px; } - .sm-input { width: 100%; padding: 8px 12px; margin-bottom: 5px; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; color: var(--fg-color); font-size: 14px; box-sizing: border-box; } - .sm-input:focus { border-color: var(--primary-color); outline: none; } - .sm-hint { font-size: 11px; color: var(--descrip-text); margin-bottom: 15px; font-style: italic; } - .sm-btn { padding: 10px 20px; background-color: var(--comfy-menu-bg); border: 1px solid var(--border-color); border-radius: 4px; color: var(--fg-color); cursor: pointer; font-size: 14px; width: 100%; transition: all 0.3s ease; } - .sm-btn:hover:not(:disabled) { background-color: var(--comfy-menu-secondary-bg); } - .sm-btn:disabled { opacity: 0.5; cursor: not-allowed; } - .sm-btn-success { background-color: #10b981; color: white; border-color: #10b981; } - .sm-btn-success:hover:not(:disabled) { background-color: #059669; } - .sm-btn-warning { background-color: #f59e0b; color: white; border-color: #f59e0b; animation: smPulse 2s infinite; } - .sm-btn-warning:hover:not(:disabled) { background-color: #d97706; } - @keyframes smPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } 50% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } } - .sm-btn-danger { background-color: #dc2626; color: white; border-color: #dc2626; } - .sm-btn-danger:hover:not(:disabled) { background-color: #b91c1c; } - .sm-backup-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 4px; margin-bottom: 8px; } - .sm-backup-name { flex: 1; color: var(--fg-color); font-size: 13px; word-break: break-all; margin-right: 10px; } - .sm-btn-small { padding: 5px 10px; font-size: 12px; min-width: 80px; width: auto; } - .sm-status-container { margin-top: auto; padding-top: 10px; min-height: 40px; display: flex; align-items: center; justify-content: center; } - .sm-status { padding: 10px 20px; border-radius: 6px; font-size: 13px; text-align: center; display: none; width: 100%; animation: smFadeIn 0.3s ease; } - @keyframes smFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } - .sm-status.sm-success { display: block; background-color: #10b981; color: white; } - .sm-status.sm-error { display: block; background-color: #ef4444; color: white; } - .sm-status.sm-info { display: block; background-color: #3b82f6; color: white; } - .sm-hidden { display: none; } - .sm-restore-container { display: flex; gap: 10px; margin-top: 10px; } - `; - container.appendChild(style); - - const mainDiv = document.createElement('div'); - mainDiv.className = 'sm-container'; - - const saveSection = document.createElement('div'); - saveSection.className = 'sm-section'; + container.innerHTML = ""; + + const mainDiv = document.createElement("div"); + mainDiv.className = "sm-container"; + + renderPresetsSection(mainDiv); + + const saveSection = document.createElement("div"); + saveSection.className = "sm-section"; saveSection.innerHTML = ` -
Save Interface Settings
+
💾 Save Interface Settings
Backup current interface configuration to Documents folder
If no name is entered, the creation date will be used.
`; - - const saveBtn = document.createElement('button'); - saveBtn.className = 'sm-btn sm-btn-success'; - saveBtn.textContent = '💾 Save Settings'; - saveBtn.onclick = async function() { - const nameInput = document.getElementById('sm-backup-name'); - const backupName = nameInput ? nameInput.value.trim() : ''; - - showStatus('Saving...', 'info'); + + const saveBtn = document.createElement("button"); + saveBtn.className = "sm-btn sm-btn-success"; + saveBtn.textContent = "💾 Save Settings"; + saveBtn.onclick = async function () { + const nameInput = document.getElementById("sm-backup-name"); + const backupName = nameInput ? nameInput.value.trim() : ""; + + showStatus("Saving...", "info"); saveBtn.disabled = true; - saveBtn.textContent = '⏳ Saving...'; - + saveBtn.textContent = "⏳ Saving..."; + try { - const response = await fetch('/rayko_settings_manager/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ backup_name: backupName }) + const response = await fetch("/rayko_settings_manager/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ backup_name: backupName }), }); const data = await response.json(); - + if (data.success) { - showStatus(`✅ Saved as: ${data.folder}`, 'success'); - if (nameInput) nameInput.value = ''; - - if (backupListDiv && !backupListDiv.classList.contains('sm-hidden')) { + showStatus(`✅ Saved as: ${data.folder}`, "success"); + if (nameInput) nameInput.value = ""; + + if (backupListDiv && !backupListDiv.classList.contains("sm-hidden")) { const newItem = createBackupItem(data.folder, backupListDiv); backupListDiv.insertBefore(newItem, backupListDiv.firstChild); } } else { - showStatus(`❌ Error: ${data.error}`, 'error'); + showStatus(`❌ Error: ${data.error}`, "error"); } } catch (e) { - showStatus(`❌ Error: ${e.message}`, 'error'); + showStatus(`❌ Error: ${e.message}`, "error"); } - + saveBtn.disabled = false; - saveBtn.textContent = '💾 Save Settings'; + saveBtn.textContent = "💾 Save Settings"; }; - + saveSection.appendChild(saveBtn); mainDiv.appendChild(saveSection); - - const restoreSection = document.createElement('div'); - restoreSection.className = 'sm-section'; + + const restoreSection = document.createElement("div"); + restoreSection.className = "sm-section"; restoreSection.innerHTML = ` -
Restore Interface Settings
+
🐦‍🔥 Restore Interface Settings
Load previously saved configuration and restart server
`; - - const loadBtn = document.createElement('button'); - loadBtn.className = 'sm-btn'; - loadBtn.textContent = ' Load Backups'; - loadBtn.onclick = async function() { + + const loadBtn = document.createElement("button"); + loadBtn.className = "sm-btn"; + loadBtn.textContent = "📂 Load Backups"; + loadBtn.onclick = async function () { await loadBackups(backupListDiv, loadBtn); }; - + restoreSection.appendChild(loadBtn); - - const backupListDiv = document.createElement('div'); - backupListDiv.id = 'sm-backup-list'; - backupListDiv.className = 'sm-hidden'; - backupListDiv.style.marginTop = '10px'; - + + const backupListDiv = document.createElement("div"); + backupListDiv.id = "sm-backup-list"; + backupListDiv.className = "sm-hidden"; + backupListDiv.style.marginTop = "10px"; + restoreSection.appendChild(backupListDiv); mainDiv.appendChild(restoreSection); - - const statusContainer = document.createElement('div'); - statusContainer.className = 'sm-status-container'; - - const statusDiv = document.createElement('div'); - statusDiv.id = 'sm-status'; - statusDiv.className = 'sm-status'; + + const statusContainer = document.createElement("div"); + statusContainer.className = "sm-status-container"; + + const statusDiv = document.createElement("div"); + statusDiv.id = "sm-status"; + statusDiv.className = "sm-status"; statusContainer.appendChild(statusDiv); - + mainDiv.appendChild(statusContainer); container.appendChild(mainDiv); } function showStatus(message, type) { - const statusDiv = document.getElementById('sm-status'); + const statusDiv = document.getElementById("sm-status"); if (statusDiv) { statusDiv.textContent = message; - statusDiv.className = 'sm-status'; - - if (type === 'success') statusDiv.classList.add('sm-success'); - else if (type === 'error') statusDiv.classList.add('sm-error'); - else if (type === 'info') statusDiv.classList.add('sm-info'); - - if (type !== 'info') { + statusDiv.className = "sm-status"; + + if (type === "success") statusDiv.classList.add("sm-success"); + else if (type === "error") statusDiv.classList.add("sm-error"); + else if (type === "info") statusDiv.classList.add("sm-info"); + + if (type !== "info") { setTimeout(() => { - statusDiv.className = 'sm-status'; - statusDiv.textContent = ''; + statusDiv.className = "sm-status"; + statusDiv.textContent = ""; }, 5000); } } } async function loadBackups(backupListDiv, loadBtn) { - showStatus('Loading backups...', 'info'); - + showStatus("Loading backups...", "info"); + loadBtn.disabled = true; - loadBtn.textContent = '⏳ Loading...'; - backupListDiv.classList.remove('sm-hidden'); + loadBtn.textContent = "⏳ Loading..."; + backupListDiv.classList.remove("sm-hidden"); backupListDiv.innerHTML = '
Loading...
'; - + try { - const response = await fetch('/rayko_settings_manager/list'); + const response = await fetch("/rayko_settings_manager/list"); const data = await response.json(); - - backupListDiv.innerHTML = ''; - + + backupListDiv.innerHTML = ""; + if (data.success && data.backups && data.backups.length > 0) { data.backups.forEach((backupName) => { const item = createBackupItem(backupName, backupListDiv); backupListDiv.appendChild(item); }); - showStatus(`✅ Found ${data.backups.length} backups`, 'success'); + showStatus(`✅ Found ${data.backups.length} backups`, "success"); } else { backupListDiv.innerHTML = '
No backups found
'; - showStatus('ℹ️ No backups found', 'info'); + showStatus("ℹ️ No backups found", "info"); } } catch (e) { backupListDiv.innerHTML = '
Error loading
'; - showStatus(`❌ Error: ${e.message}`, 'error'); + showStatus(`❌ Error: ${e.message}`, "error"); } - + loadBtn.disabled = false; - loadBtn.textContent = '🔄 Refresh Backups'; + loadBtn.textContent = "🔄 Refresh Backups"; } function createBackupItem(backupName, backupListDiv) { - const item = document.createElement('div'); - item.className = 'sm-backup-item'; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'sm-backup-name'; + const item = document.createElement("div"); + item.className = "sm-backup-item"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "sm-backup-name"; nameSpan.textContent = backupName; nameSpan.title = backupName; item.appendChild(nameSpan); - - const buttonsDiv = document.createElement('div'); - buttonsDiv.className = 'sm-restore-container'; - - const restoreBtn = document.createElement('button'); - restoreBtn.className = 'sm-btn sm-btn-small'; - restoreBtn.textContent = '✓ Restore'; - restoreBtn.onclick = async function() { - showStatus('Restoring...', 'info'); + + const buttonsDiv = document.createElement("div"); + buttonsDiv.className = "sm-restore-container"; + + const restoreBtn = document.createElement("button"); + restoreBtn.className = "sm-btn sm-btn-small"; + restoreBtn.textContent = "✓ Restore"; + restoreBtn.onclick = async function () { + showStatus("Restoring...", "info"); restoreBtn.disabled = true; - restoreBtn.textContent = '⏳...'; - + restoreBtn.textContent = "⏳..."; + try { - const res = await fetch('/rayko_settings_manager/restore', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ backup_name: backupName }) + const res = await fetch("/rayko_settings_manager/restore", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ backup_name: backupName }), }); const data = await res.json(); - + if (data.success) { - showStatus(`✅ Settings restored from ${backupName}`, 'success'); - restoreBtn.textContent = '✓ Restored'; + showStatus(`✅ Settings restored from ${backupName}`, "success"); + restoreBtn.textContent = "✓ Restored"; restoreBtn.disabled = true; - - const restartBtn = document.createElement('button'); - restartBtn.className = 'sm-btn sm-btn-small sm-btn-warning'; - restartBtn.textContent = '🔄 RESTART SERVER'; - restartBtn.onclick = async function() { + + const restartBtn = document.createElement("button"); + restartBtn.className = "sm-btn sm-btn-small sm-btn-warning"; + restartBtn.textContent = "🔄 RESTART SERVER"; + restartBtn.onclick = async function () { restartBtn.disabled = true; - restartBtn.textContent = ' Restarting...'; - showStatus('Server restarting...', 'info'); - + restartBtn.textContent = "⏳ Restarting..."; + showStatus("Server restarting...", "info"); + try { - await fetch('/rayko_settings_manager/restart', { method: 'POST' }); - } catch (e) {} - + await fetch("/rayko_settings_manager/restart", { method: "POST" }); + } catch (e) { } + startPolling(restartBtn); }; - + buttonsDiv.appendChild(restartBtn); } else { - showStatus(`❌ Error: ${data.error}`, 'error'); + showStatus(`❌ Error: ${data.error}`, "error"); restoreBtn.disabled = false; - restoreBtn.textContent = '✓ Restore'; + restoreBtn.textContent = "✓ Restore"; } } catch (e) { - showStatus(`❌ Error: ${e.message}`, 'error'); + showStatus(`❌ Error: ${e.message}`, "error"); restoreBtn.disabled = false; - restoreBtn.textContent = '✓ Restore'; + restoreBtn.textContent = "✓ Restore"; } }; - - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'sm-btn sm-btn-small sm-btn-danger'; - deleteBtn.textContent = '✕'; - deleteBtn.title = 'Delete backup'; - deleteBtn.onclick = async function() { - showStatus('Deleting...', 'info'); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "sm-btn sm-btn-small sm-btn-danger"; + deleteBtn.textContent = "✕"; + deleteBtn.title = "Delete backup"; + deleteBtn.onclick = async function () { + showStatus("Deleting...", "info"); deleteBtn.disabled = true; - deleteBtn.textContent = '⏳'; - + deleteBtn.textContent = "⏳"; + try { const res = await fetch(`/rayko_settings_manager/delete/${encodeURIComponent(backupName)}`, { - method: 'DELETE' + method: "DELETE", }); const data = await res.json(); - + if (data.success) { item.remove(); - showStatus(`✅ Deleted: ${backupName}`, 'success'); - + showStatus(`✅ Deleted: ${backupName}`, "success"); + if (backupListDiv.children.length === 0) { backupListDiv.innerHTML = '
No backups found
'; } } else { - showStatus(`❌ Error: ${data.error}`, 'error'); + showStatus(`❌ Error: ${data.error}`, "error"); deleteBtn.disabled = false; - deleteBtn.textContent = '✕'; + deleteBtn.textContent = "✕"; } } catch (e) { - showStatus(`❌ Error: ${e.message}`, 'error'); + showStatus(`❌ Error: ${e.message}`, "error"); deleteBtn.disabled = false; - deleteBtn.textContent = '✕'; + deleteBtn.textContent = "✕"; } }; - + buttonsDiv.appendChild(restoreBtn); buttonsDiv.appendChild(deleteBtn); item.appendChild(buttonsDiv); - + return item; } @@ -491,18 +520,18 @@ function startPolling(restartBtn) { let attempts = 0; const maxAttempts = 10; const interval = 5000; - + const pollInterval = setInterval(async () => { attempts++; - showStatus(`Waiting for server... (${attempts}/${maxAttempts})`, 'info'); - + showStatus(`Waiting for server... (${attempts}/${maxAttempts})`, "info"); + try { - const res = await fetch('/rayko_settings_manager/ping'); + const res = await fetch("/rayko_settings_manager/ping"); const data = await res.json(); - - if (data.status === 'ok') { + + if (data.status === "ok") { clearInterval(pollInterval); - showStatus('✅ Server is ready! Refreshing page...', 'success'); + showStatus("✅ Server is ready! Refreshing page...", "success"); setTimeout(() => { window.location.reload(); }, 2000); @@ -510,10 +539,625 @@ function startPolling(restartBtn) { } catch (e) { if (attempts >= maxAttempts) { clearInterval(pollInterval); - showStatus('️ Server may not have restarted. Please refresh manually.', 'error'); + showStatus("⚠️ Server may not have restarted. Please refresh manually.", "error"); restartBtn.disabled = false; - restartBtn.textContent = '🔄 RESTART SERVER'; + restartBtn.textContent = "🔄 RESTART SERVER"; } } }, interval); +} + +function renderPresetsSection(mainDiv) { + const section = document.createElement("div"); + section.className = "sm-section"; + + const banner = document.createElement("div"); + banner.id = "sm-active-preset-banner"; + banner.className = "sm-active-banner sm-hidden"; + banner.innerHTML = ` +
+ Active Preset: +
+ `; + + const updateBtn = document.createElement("button"); + updateBtn.className = "sm-btn sm-btn-primary sm-btn-small"; + updateBtn.textContent = "💾 Update This Preset"; + updateBtn.onclick = async () => { + if (!_activePresetName) return; + updateBtn.disabled = true; + updateBtn.textContent = "⏳ Saving..."; + try { + await saveCurrentStateToActivePreset(); + showStatus(`✅ Preset "${_activePresetName}" updated!`, "success"); + } catch (e) { + showStatus(`❌ ${e.message}`, "error"); + } + updateBtn.disabled = false; + updateBtn.textContent = "💾 Update This Preset"; + }; + banner.appendChild(updateBtn); + section.appendChild(banner); + + const formDiv = document.createElement("div"); + formDiv.innerHTML = ` +
🖥️ Workflow Presets
+
Save all currently open workflow tabs as a preset, or load a preset to open its workflows as new tabs
+ +
Presets are stored in custom_nodes/ComfyUI_RaykoStudio/presets_workflow
+ `; + section.appendChild(formDiv); + + const saveBtn = document.createElement("button"); + saveBtn.className = "sm-btn sm-btn-success"; + saveBtn.textContent = "💾 Save Open Tabs as New Preset"; + saveBtn.style.marginBottom = "10px"; + saveBtn.onclick = async () => { + const nameInput = section.querySelector("#sm-preset-name"); + const name = (nameInput?.value || "").trim(); + if (!name) { + showStatus("Enter preset name", "error"); + return; + } + + const workflows = getOpenWorkflowsData(); + if (workflows.length === 0) { + showStatus("No valid open workflows to save (Unsaved filtered)", "error"); + return; + } + + saveBtn.disabled = true; + saveBtn.textContent = "⏳ Saving..."; + try { + const res = await fetch("/rayko_settings_manager/presets/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, workflows }), + }); + const data = await res.json(); + if (data.success) { + showStatus(`✅ Preset "${data.name}" saved (${data.count} workflows)`, "success"); + nameInput.value = ""; + setActivePreset(data.name); + const list = section.querySelector("#sm-preset-list"); + if (list && !list.classList.contains("sm-hidden")) { + await refreshPresetList(list); + } + } else { + showStatus(`❌ ${data.error || "Save failed"}`, "error"); + } + } catch (e) { + showStatus(`❌ ${e.message}`, "error"); + } + saveBtn.disabled = false; + saveBtn.textContent = "💾 Save Open Tabs as New Preset"; + }; + section.appendChild(saveBtn); + + const toggleBtn = document.createElement("button"); + toggleBtn.className = "sm-btn"; + toggleBtn.textContent = "📂 Show Presets"; + toggleBtn.onclick = async () => { + const list = section.querySelector("#sm-preset-list"); + list.classList.toggle("sm-hidden"); + if (!list.classList.contains("sm-hidden")) { + await refreshPresetList(list); + } + }; + section.appendChild(toggleBtn); + + const list = document.createElement("div"); + list.id = "sm-preset-list"; + list.className = "sm-hidden"; + list.style.marginTop = "10px"; + section.appendChild(list); + + mainDiv.appendChild(section); +} + +function updatePresetUI() { + const banner = document.getElementById("sm-active-preset-banner"); + const nameSpan = document.getElementById("sm-active-preset-name"); + if (banner && nameSpan) { + if (_activePresetName) { + nameSpan.textContent = `"${_activePresetName}"`; + banner.classList.remove("sm-hidden"); + } else { + banner.classList.add("sm-hidden"); + } + } +} + +function getOpenWorkflowsData() { + const result = []; + + try { + const wfStore = app.extensionManager?.workflow; + const openWorkflows = wfStore?.openWorkflows; + + if (Array.isArray(openWorkflows) && openWorkflows.length > 0) { + for (const wf of openWorkflows) { + try { + if (!wf.changeTracker) continue; + + const wfName = wf.name || ""; + const wfPath = wf.path || ""; + + if (wfName.includes("Unsaved") || wfPath.includes("Unsaved")) { + continue; + } + + const state = wf.activeState || (wf.changeTracker && wf.changeTracker.activeState); + if (state && state.nodes) { + result.push({ + name: wfName || wfPath || "Workflow", + data: JSON.parse(JSON.stringify(state)), + }); + } + } catch (e) { + console.warn("[Rayko] Failed to serialize workflow", wf, e); + } + } + } + } catch (e) { + console.warn("[Rayko] Workflow store access failed", e); + } + + return result; +} + +async function loadPresetAsNewTabs(presetName) { + const wfStore = app.extensionManager?.workflow; + + if (_activePresetName && _activePresetName !== presetName) { + const action = await smConfirmSavePreset(_activePresetName); + if (action === "cancel") return 0; + if (action === "save") { + try { + await saveCurrentStateToActivePreset(); + } catch (e) { + await smAlert("Save Failed", e.message, true); + return 0; + } + } + } else if (!_activePresetName) { + const confirmed = await smConfirm({ + title: "Load Preset", + message: `Loading preset "${presetName}" will replace all currently open workflows. Continue?`, + okText: "Load & Replace", + danger: true + }); + if (!confirmed) return 0; + } + + await performCloseAll(); + + const res = await fetch(`/rayko_settings_manager/presets/get/${encodeURIComponent(presetName)}`); + if (!res.ok) throw new Error("Preset not found"); + const data = await res.json(); + if (!data.workflows || data.workflows.length === 0) throw new Error("Preset is empty"); + + const activeWf = wfStore.activeWorkflow; + let loaded = 0; + + if (activeWf && data.workflows.length > 0) { + const first = data.workflows[0]; + try { + await app.loadGraphData(first.data, true, true, activeWf); + if (first.name) { + if (typeof wfStore.renameWorkflow === "function") { + await wfStore.renameWorkflow(activeWf, first.name); + } else if ("path" in activeWf) { + activeWf.path = first.name; + } + } + loaded++; + } catch (e) { + console.error("[Rayko] Failed to load first preset workflow", e); + } + } + + for (let i = 1; i < data.workflows.length; i++) { + const item = data.workflows[i]; + try { + let targetWf = null; + if (typeof wfStore.createTemporaryWorkflow === "function") { + targetWf = await wfStore.createTemporaryWorkflow(item.name); + } else if (typeof wfStore.createTemporary === "function") { + targetWf = await wfStore.createTemporary(); + } + + await app.loadGraphData(item.data, true, true, targetWf || undefined); + + if (targetWf) { + if (typeof wfStore.renameWorkflow === "function") { + await wfStore.renameWorkflow(targetWf, item.name); + } else if ("path" in targetWf) { + targetWf.path = item.name; + } + } + loaded++; + } catch (e) { + console.error("[Rayko] Failed to load workflow", item.name, e); + } + } + + if (loaded === 0) throw new Error("Failed to load any workflow"); + + setActivePreset(presetName); + return loaded; +} + +async function refreshPresetList(listEl) { + listEl.innerHTML = '
Loading...
'; + try { + const res = await fetch("/rayko_settings_manager/presets/list"); + const data = await res.json(); + listEl.innerHTML = ""; + + if (!data.success || !data.presets || data.presets.length === 0) { + listEl.innerHTML = '
No presets
'; + return; + } + + for (const p of data.presets) { + const item = document.createElement("div"); + item.className = "sm-backup-item"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "sm-backup-name"; + nameSpan.textContent = `${p.name} (${p.count})`; + nameSpan.title = `${p.count} workflows`; + item.appendChild(nameSpan); + + const btns = document.createElement("div"); + btns.className = "sm-restore-container"; + + const loadBtn = document.createElement("button"); + loadBtn.className = "sm-btn sm-btn-small"; + loadBtn.textContent = "📂 Load"; + loadBtn.onclick = async () => { + loadBtn.disabled = true; + loadBtn.textContent = "⏳..."; + try { + const count = await loadPresetAsNewTabs(p.name); + if (count > 0) { + showStatus(`✅ Loaded "${p.name}" (${count} workflows)`, "success"); + const overlay = document.getElementById("sm-modal-overlay"); + if (overlay) overlay.remove(); + } else { + loadBtn.disabled = false; + loadBtn.textContent = "📂 Load"; + } + } catch (e) { + showStatus(`❌ ${e.message}`, "error"); + loadBtn.disabled = false; + loadBtn.textContent = "📂 Load"; + } + }; + btns.appendChild(loadBtn); + + const delBtn = document.createElement("button"); + delBtn.className = "sm-btn sm-btn-small sm-btn-danger"; + delBtn.textContent = "✕"; + delBtn.title = "Delete preset"; + delBtn.onclick = async () => { + if (!confirm(`Delete preset "${p.name}"?`)) return; + delBtn.disabled = true; + try { + const r = await fetch(`/rayko_settings_manager/presets/delete/${encodeURIComponent(p.name)}`, { + method: "DELETE", + }); + const d = await r.json(); + if (d.success) { + item.remove(); + showStatus(`✅ Deleted "${p.name}"`, "success"); + if (_activePresetName === p.name) setActivePreset(null); + } else { + showStatus(`❌ ${d.error || "Delete failed"}`, "error"); + } + } catch (e) { + showStatus(`❌ ${e.message}`, "error"); + } + delBtn.disabled = false; + }; + btns.appendChild(delBtn); + + item.appendChild(btns); + listEl.appendChild(item); + } + } catch (e) { + listEl.innerHTML = `
Error: ${e.message}
`; + } +} + +function smConfirmSavePreset(presetName) { + return new Promise((resolve) => { + const overlay = document.createElement("div"); + overlay.className = "sm-modal-overlay"; + overlay.style.zIndex = "1000000"; + + const modal = document.createElement("div"); + modal.className = "sm-modal-content"; + modal.style.maxWidth = "400px"; + + const title = document.createElement("div"); + title.className = "sm-title"; + title.textContent = "Save Changes?"; + + const msg = document.createElement("div"); + msg.className = "sm-description"; + msg.style.marginBottom = "20px"; + msg.style.lineHeight = "1.5"; + msg.innerHTML = `You have an active preset: "${presetName}".

Do you want to save the current workflows to this preset before continuing?`; + + const btnRow = document.createElement("div"); + btnRow.style.display = "flex"; + btnRow.style.flexDirection = "column"; + btnRow.style.gap = "10px"; + + const cancelBtn = document.createElement("button"); + cancelBtn.className = "sm-btn"; + cancelBtn.textContent = "Cancel"; + cancelBtn.onclick = () => { overlay.remove(); resolve("cancel"); }; + + const saveBtn = document.createElement("button"); + saveBtn.className = "sm-btn sm-btn-success"; + saveBtn.textContent = "💾 Save & Continue"; + saveBtn.onclick = () => { overlay.remove(); resolve("save"); }; + + const discardBtn = document.createElement("button"); + discardBtn.className = "sm-btn sm-btn-danger"; + discardBtn.textContent = "Discard Changes & Continue"; + discardBtn.onclick = () => { overlay.remove(); resolve("discard"); }; + + btnRow.appendChild(saveBtn); + btnRow.appendChild(discardBtn); + btnRow.appendChild(cancelBtn); + + modal.appendChild(title); + modal.appendChild(msg); + modal.appendChild(btnRow); + overlay.appendChild(modal); + document.body.appendChild(overlay); + }); +} + +function smConfirm(opts) { + return new Promise((resolve) => { + const overlay = document.createElement("div"); + overlay.className = "sm-modal-overlay"; + overlay.style.zIndex = "1000000"; + + const modal = document.createElement("div"); + modal.className = "sm-modal-content"; + modal.style.maxWidth = "350px"; + + const title = document.createElement("div"); + title.className = "sm-title"; + title.textContent = opts.title || "Confirm"; + + const msg = document.createElement("div"); + msg.className = "sm-description"; + msg.style.marginBottom = "20px"; + msg.style.lineHeight = "1.5"; + msg.textContent = opts.message || "Are you sure?"; + + const btnRow = document.createElement("div"); + btnRow.style.display = "flex"; + btnRow.style.gap = "10px"; + + const cancelBtn = document.createElement("button"); + cancelBtn.className = "sm-btn"; + cancelBtn.textContent = "Cancel"; + cancelBtn.onclick = () => { overlay.remove(); resolve(false); }; + + const okBtn = document.createElement("button"); + okBtn.className = opts.danger ? "sm-btn sm-btn-danger" : "sm-btn sm-btn-success"; + okBtn.textContent = opts.okText || "OK"; + okBtn.onclick = () => { overlay.remove(); resolve(true); }; + + btnRow.appendChild(cancelBtn); + btnRow.appendChild(okBtn); + + modal.appendChild(title); + modal.appendChild(msg); + modal.appendChild(btnRow); + overlay.appendChild(modal); + document.body.appendChild(overlay); + }); +} + +function smAlert(titleText, message, danger) { + return new Promise((resolve) => { + const overlay = document.createElement("div"); + overlay.className = "sm-modal-overlay"; + overlay.style.zIndex = "1000000"; + + const modal = document.createElement("div"); + modal.className = "sm-modal-content"; + modal.style.maxWidth = "350px"; + + const title = document.createElement("div"); + title.className = "sm-title"; + title.textContent = titleText || "Alert"; + + const msg = document.createElement("div"); + msg.className = "sm-description"; + msg.style.marginBottom = "20px"; + msg.style.lineHeight = "1.5"; + msg.textContent = message || ""; + + const btnRow = document.createElement("div"); + btnRow.style.display = "flex"; + btnRow.style.gap = "10px"; + + const okBtn = document.createElement("button"); + okBtn.className = danger ? "sm-btn sm-btn-danger" : "sm-btn sm-btn-primary"; + okBtn.textContent = "OK"; + okBtn.onclick = () => { overlay.remove(); resolve(); }; + + btnRow.appendChild(okBtn); + modal.appendChild(title); + modal.appendChild(msg); + modal.appendChild(btnRow); + overlay.appendChild(modal); + document.body.appendChild(overlay); + }); +} + +function injectStyles() { + if (document.getElementById("sm-styles")) return; + const style = document.createElement("style"); + style.id = "sm-styles"; + style.textContent = ` + /* ---------- Topbar block ---------- */ + .sm-topbar-block { + display: inline-flex; + align-items: stretch; + align-self: center; + flex: 0 0 auto; + height: 32px; + margin: 0 0 0 5px; + background: #2b2a32; + border-radius: 6px; + overflow: hidden; + box-sizing: border-box; + } + .sm-topbar-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 3px; + padding: 0 10px; + margin: 0; + background: transparent; + border: none; + color: var(--fg-color); + cursor: pointer; + transition: background 0.15s; + height: 100%; + font-family: inherit; + font-size: 13px; + box-sizing: border-box; + } + .sm-topbar-btn:hover:not(:disabled) { + background: #52525e; + } + .sm-topbar-btn:active:not(:disabled) { + background: #52525e; + filter: brightness(0.92); + } + .sm-topbar-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .sm-topbar-icon { + font-size: 16px; + line-height: 1; + } + + /* ---------- Modal ---------- */ + .sm-modal-overlay { + position: fixed; inset: 0; + background: rgba(0,0,0,0.6); + display: flex; justify-content: center; align-items: center; + z-index: 999999; + } + .sm-modal-content { + background: var(--comfy-menu-bg); + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 20px; + width: 90%; max-width: 480px; + max-height: 90vh; overflow-y: auto; + box-shadow: 0 8px 24px rgba(0,0,0,0.4); + position: relative; + } + .sm-container { display: flex; flex-direction: column; gap: 20px; min-height: 300px; } + .sm-section { + background-color: var(--comfy-input-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 15px; + } + .sm-title { font-size: 16px; font-weight: bold; margin-bottom: 10px; color: var(--fg-color); } + .sm-description { font-size: 12px; color: var(--descrip-text); margin-bottom: 15px; } + .sm-input { + width: 100%; padding: 8px 12px; margin-bottom: 5px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); border-radius: 4px; + color: var(--fg-color); font-size: 14px; box-sizing: border-box; + } + .sm-input:focus { border-color: var(--primary-color); outline: none; } + .sm-hint { font-size: 11px; color: var(--descrip-text); margin-bottom: 15px; font-style: italic; } + .sm-btn { + padding: 10px 20px; + background-color: var(--comfy-menu-bg); + border: 1px solid var(--border-color); + border-radius: 4px; color: var(--fg-color); + cursor: pointer; font-size: 14px; width: 100%; + transition: all 0.3s ease; + } + .sm-btn:hover:not(:disabled) { background-color: var(--comfy-menu-secondary-bg); } + .sm-btn:disabled { opacity: 0.5; cursor: not-allowed; } + .sm-btn-success { background-color: #10b981; color: white; border-color: #10b981; } + .sm-btn-success:hover:not(:disabled) { background-color: #059669; } + .sm-btn-primary { background-color: #3b82f6; color: white; border-color: #3b82f6; } + .sm-btn-primary:hover:not(:disabled) { background-color: #2563eb; } + .sm-btn-warning { + background-color: #f59e0b; color: white; border-color: #f59e0b; + animation: smPulse 2s infinite; + } + .sm-btn-warning:hover:not(:disabled) { background-color: #d97706; } + @keyframes smPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } + 50% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } + } + .sm-btn-danger { background-color: #dc2626; color: white; border-color: #dc2626; } + .sm-btn-danger:hover:not(:disabled) { background-color: #b91c1c; } + .sm-backup-item { + display: flex; justify-content: space-between; align-items: center; + padding: 10px; + background-color: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 4px; margin-bottom: 8px; + } + .sm-backup-name { + flex: 1; color: var(--fg-color); font-size: 13px; + word-break: break-all; margin-right: 10px; + } + .sm-btn-small { padding: 5px 10px; font-size: 12px; min-width: 80px; width: auto; } + .sm-status-container { + margin-top: auto; padding-top: 10px; min-height: 40px; + display: flex; align-items: center; justify-content: center; + } + .sm-status { + padding: 10px 20px; border-radius: 6px; font-size: 13px; + text-align: center; display: none; width: 100%; + animation: smFadeIn 0.3s ease; + } + @keyframes smFadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + .sm-status.sm-success { display: block; background-color: #10b981; color: white; } + .sm-status.sm-error { display: block; background-color: #ef4444; color: white; } + .sm-status.sm-info { display: block; background-color: #3b82f6; color: white; } + .sm-hidden { display: none; } + .sm-restore-container { display: flex; gap: 10px; margin-top: 10px; } + + /* ---------- Active Preset Banner ---------- */ + .sm-active-banner { + background: rgba(16, 185, 129, 0.15); + border: 1px solid #10b981; + border-radius: 8px; + padding: 12px; + margin-bottom: 15px; + color: var(--fg-color); + font-size: 13px; + animation: smFadeIn 0.3s ease; + } + `; + document.head.appendChild(style); } \ No newline at end of file