import { app } from "../../../scripts/app.js"; 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() { injectStyles(); app.ui.settings.addSetting({ id: "RaykoStudio.SettingsManager.Open", name: "Settings Manager", type: () => buildSettingsEntry(), }); 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"; 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 waitForActionBar(callback) { const existing = document.querySelector(".actionbar-container"); if (existing) return callback(existing); 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); } function injectTopbarButton(actionBar) { if (localStorage.getItem(STORAGE_KEY) !== "true") return; if (document.getElementById("sm-topbar-block")) return; if (!actionBar || !document.body.contains(actionBar)) return; const block = document.createElement("div"); block.id = "sm-topbar-block"; block.className = "sm-topbar-block"; const btn = document.createElement("button"); btn.className = "sm-topbar-btn"; btn.title = "Settings Manager"; btn.innerHTML = `πŸ–₯️`; btn.onclick = () => openSettingsManagerModal(); block.appendChild(btn); 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 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 = () => overlay.remove(); renderSettingsPanel(modal); modal.appendChild(closeBtn); overlay.appendChild(modal); document.body.appendChild(overlay); updatePresetUI(); } function renderSettingsPanel(container) { 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
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"); saveBtn.disabled = true; 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 data = await response.json(); if (data.success) { 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"); } } catch (e) { showStatus(`❌ Error: ${e.message}`, "error"); } saveBtn.disabled = false; saveBtn.textContent = "πŸ’Ύ Save Settings"; }; saveSection.appendChild(saveBtn); mainDiv.appendChild(saveSection); const restoreSection = document.createElement("div"); restoreSection.className = "sm-section"; restoreSection.innerHTML = `
πŸ¦β€πŸ”₯ 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 () { 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"; 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"; statusContainer.appendChild(statusDiv); mainDiv.appendChild(statusContainer); container.appendChild(mainDiv); } function showStatus(message, type) { 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") { setTimeout(() => { statusDiv.className = "sm-status"; statusDiv.textContent = ""; }, 5000); } } } async function loadBackups(backupListDiv, loadBtn) { showStatus("Loading backups...", "info"); loadBtn.disabled = true; loadBtn.textContent = "⏳ Loading..."; backupListDiv.classList.remove("sm-hidden"); backupListDiv.innerHTML = '
Loading...
'; try { const response = await fetch("/rayko_settings_manager/list"); const data = await response.json(); 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"); } else { backupListDiv.innerHTML = '
No backups found
'; showStatus("ℹ️ No backups found", "info"); } } catch (e) { backupListDiv.innerHTML = '
Error loading
'; showStatus(`❌ Error: ${e.message}`, "error"); } loadBtn.disabled = false; 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"; 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"); restoreBtn.disabled = true; 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 data = await res.json(); if (data.success) { 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 () { restartBtn.disabled = true; restartBtn.textContent = "⏳ Restarting..."; showStatus("Server restarting...", "info"); try { await fetch("/rayko_settings_manager/restart", { method: "POST" }); } catch (e) { } startPolling(restartBtn); }; buttonsDiv.appendChild(restartBtn); } else { showStatus(`❌ Error: ${data.error}`, "error"); restoreBtn.disabled = false; restoreBtn.textContent = "βœ“ Restore"; } } catch (e) { showStatus(`❌ Error: ${e.message}`, "error"); restoreBtn.disabled = false; 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"); deleteBtn.disabled = true; deleteBtn.textContent = "⏳"; try { const res = await fetch(`/rayko_settings_manager/delete/${encodeURIComponent(backupName)}`, { method: "DELETE", }); const data = await res.json(); if (data.success) { item.remove(); showStatus(`βœ… Deleted: ${backupName}`, "success"); if (backupListDiv.children.length === 0) { backupListDiv.innerHTML = '
No backups found
'; } } else { showStatus(`❌ Error: ${data.error}`, "error"); deleteBtn.disabled = false; deleteBtn.textContent = "βœ•"; } } catch (e) { showStatus(`❌ Error: ${e.message}`, "error"); deleteBtn.disabled = false; deleteBtn.textContent = "βœ•"; } }; buttonsDiv.appendChild(restoreBtn); buttonsDiv.appendChild(deleteBtn); item.appendChild(buttonsDiv); return item; } function startPolling(restartBtn) { let attempts = 0; const maxAttempts = 10; const interval = 5000; const pollInterval = setInterval(async () => { attempts++; showStatus(`Waiting for server... (${attempts}/${maxAttempts})`, "info"); try { const res = await fetch("/rayko_settings_manager/ping"); const data = await res.json(); if (data.status === "ok") { clearInterval(pollInterval); showStatus("βœ… Server is ready! Refreshing page...", "success"); setTimeout(() => { window.location.reload(); }, 2000); } } catch (e) { if (attempts >= maxAttempts) { clearInterval(pollInterval); showStatus("⚠️ Server may not have restarted. Please refresh manually.", "error"); restartBtn.disabled = false; 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); }