Files

211 lines
6.1 KiB
JavaScript
Raw Permalink 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 { API_ROOT, COLORS, EMPTY_VALUE } from "./constants.js";
import {
findWidget,
flashButton,
patchFileDropdown,
registerNode,
setWidgetValue,
waitForWidgets,
} from "./utils.js";
import { ButtonRowWidget } from "./widgets/button_row_widget.js";
// --- Constants ---
const NODE_NAME = "CharacterLoader";
const BASE_ENDPOINT = `${API_ROOT}/characters`;
const CHARACTER_TEMPLATE = {
eyes: "",
hair_type: "",
hair_style: "",
makeup: "",
jewelry: "",
body_type: "",
};
const FIELDS = Object.keys(CHARACTER_TEMPLATE);
// --- JSON <-> Widgets sync ---
const buildJsonFromWidgets = (node) =>
JSON.stringify(
Object.fromEntries(
FIELDS.map((f) => [f, findWidget(node, f)?.value ?? ""]),
),
null,
2,
);
const pushJsonToWidgets = (node, data) => {
for (const field of FIELDS) {
setWidgetValue(findWidget(node, field), data[field] ?? "");
}
if (node.graph) node.graph.setDirtyCanvas(true, true);
};
const clearWidgets = (node) => {
pushJsonToWidgets(node, CHARACTER_TEMPLATE);
};
// --- File loading ---
const loadFileIntoWidgets = async (file, node) => {
if (!file || file === EMPTY_VALUE) return;
try {
const res = await fetch(
`${BASE_ENDPOINT}/load?file=${encodeURIComponent(file)}`,
);
const data = await res.json();
if (data.error) {
console.warn(`[${NODE_NAME}] Load error:`, data.error);
return;
}
pushJsonToWidgets(node, JSON.parse(data.content));
} catch (e) {
console.warn(`[${NODE_NAME}] Failed to load or parse file:`, e);
}
};
// --- Buttons ---
const handleNew = async (node, characterFileWidget) => {
const name = prompt("New character file name (e.g. characters/Alice):");
if (!name?.trim()) return;
const res = await fetch(`${BASE_ENDPOINT}/new`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file: name.trim() }),
});
const data = await res.json();
if (data.ok) {
patchFileDropdown(characterFileWidget, EMPTY_VALUE, data.file);
pushJsonToWidgets(node, JSON.parse(data.content));
if (node.graph) node.graph.setDirtyCanvas(true, true);
} else {
alert(`[${NODE_NAME}] ${data.error}`);
}
};
const handleSave = async (node, characterFileWidget) => {
const file = characterFileWidget.value;
if (!file || file === EMPTY_VALUE) {
alert(`[${NODE_NAME}] No character file selected.`);
return;
}
const content = buildJsonFromWidgets(node);
const res = await fetch(`${BASE_ENDPOINT}/save`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file, content }),
});
const data = await res.json();
if (data.ok) {
flashButton(node, 1, "✅ Saved!");
} else {
alert(`[${NODE_NAME}] Save failed: ${data.error}`);
}
};
const handleClone = async (node, characterFileWidget) => {
const name = prompt("Clone to new file (e.g. characters/Alice_v2):");
if (!name?.trim()) return;
const content = buildJsonFromWidgets(node);
const file = name.trim().endsWith(".json")
? name.trim()
: `${name.trim()}.json`;
const res = await fetch(`${BASE_ENDPOINT}/save`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file, content }),
});
const data = await res.json();
if (data.ok) {
patchFileDropdown(characterFileWidget, EMPTY_VALUE, file);
if (node.graph) node.graph.setDirtyCanvas(true, true);
} else {
alert(`[${NODE_NAME}] Clone failed: ${data.error}`);
}
};
const handleDelete = async (node, characterFileWidget) => {
const file = characterFileWidget.value;
if (!file || file === EMPTY_VALUE) {
alert(`[${NODE_NAME}] No character file selected.`);
return;
}
if (!confirm(`Delete "${file}"? This cannot be undone.`)) return;
const res = await fetch(`${BASE_ENDPOINT}/delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file }),
});
const data = await res.json();
if (data.ok) {
const values = characterFileWidget.options?.values ?? [];
const idx = values.indexOf(file);
if (idx !== -1) values.splice(idx, 1);
if (!values.length) values.push(EMPTY_VALUE);
characterFileWidget.options.values = values;
characterFileWidget.value = values[0];
characterFileWidget.callback?.(values[0]);
if (values[0] === EMPTY_VALUE) clearWidgets(node);
if (node.graph) node.graph.setDirtyCanvas(true, true);
} else {
alert(`[${NODE_NAME}] Delete failed: ${data.error}`);
}
};
// --- Attach ---
const attachCharacterLoader = (node) => {
const characterFileWidget = findWidget(node, "character_file");
if (!characterFileWidget) return;
node.widgets.push(
new ButtonRowWidget("action_buttons", [
{
label: "➕ New",
onClick: () => handleNew(node, characterFileWidget),
},
{
label: "💾 Save",
onClick: () => handleSave(node, characterFileWidget),
},
{
label: "📋 Clone",
onClick: () => handleClone(node, characterFileWidget),
},
{
label: "🗑️ Delete",
color: COLORS.dark_red,
onClick: () => handleDelete(node, characterFileWidget),
},
]),
);
// Load initial file
loadFileIntoWidgets(characterFileWidget.value, node);
// Reload on file change
const originalCallback = characterFileWidget.callback;
characterFileWidget.callback = function (value) {
if (originalCallback) originalCallback.call(this, value);
loadFileIntoWidgets(value, node);
};
};
// --- Registration ---
registerNode(NODE_NAME, (node) => waitForWidgets(node, attachCharacterLoader));