Files
Raykosan-ComfyUI_RaykoStudio/web/settings_manager.js
T

1163 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `<span class="sm-topbar-icon">🖥️</span>`;
btn.onclick = () => openSettingsManagerModal();
block.appendChild(btn);
const closeAllBtn = document.createElement("button");
closeAllBtn.className = "sm-topbar-btn";
closeAllBtn.title = "Close All Workflows";
closeAllBtn.innerHTML = `<span class="sm-topbar-icon">🗑️</span>`;
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 = `
<div class="sm-title">💾 Save Interface Settings</div>
<div class="sm-description">Backup current interface configuration to Documents folder</div>
<input type="text" id="sm-backup-name" class="sm-input" placeholder="name backup">
<div class="sm-hint">If no name is entered, the creation date will be used.</div>
`;
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 = `
<div class="sm-title">🐦‍🔥 Restore Interface Settings</div>
<div class="sm-description">Load previously saved configuration and restart server</div>
`;
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 = '<div style="text-align:center;padding:20px;color:var(--descrip-text)">Loading...</div>';
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 = '<div style="text-align:center;padding:20px;color:var(--descrip-text)">No backups found</div>';
showStatus("ℹ️ No backups found", "info");
}
} catch (e) {
backupListDiv.innerHTML = '<div style="text-align:center;padding:20px;color:var(--descrip-text)">Error loading</div>';
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 = '<div style="text-align:center;padding:20px;color:var(--descrip-text)">No backups found</div>';
}
} 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 = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<span>Active Preset: <strong id="sm-active-preset-name"></strong></span>
</div>
`;
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 = `
<div class="sm-title">🖥️ Workflow Presets</div>
<div class="sm-description">Save all currently open workflow tabs as a preset, or load a preset to open its workflows as new tabs</div>
<input type="text" id="sm-preset-name" class="sm-input" placeholder="preset name">
<div class="sm-hint">Presets are stored in custom_nodes/ComfyUI_RaykoStudio/presets_workflow</div>
`;
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 = '<div style="text-align:center;padding:10px;color:var(--descrip-text)">Loading...</div>';
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 = '<div style="text-align:center;padding:10px;color:var(--descrip-text)">No presets</div>';
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 = `<div style="color:#ef4444;padding:10px">Error: ${e.message}</div>`;
}
}
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: <strong>"${presetName}"</strong>.<br><br>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);
}