Files

212 lines
8.0 KiB
JavaScript

import { app } from "../../scripts/app.js";
console.log("[SmartResolution] keywords_editor.js loaded");
const API = "/smart_resolution";
const STYLE = {
overlay: `
position: fixed; inset: 0; background: rgba(0,0,0,0.75);
display: flex; align-items: center; justify-content: center; z-index: 9999;`,
dialog: `
background: #1a1a2e; color: #e0e0e0; border: 1px solid #444;
border-radius: 8px; padding: 20px; width: 860px; max-width: 95vw;
max-height: 82vh; display: flex; flex-direction: column; gap: 12px;
font-family: monospace; font-size: 13px;`,
input: `
background: #252535; color: #e0e0e0; border: 1px solid #555;
border-radius: 3px; padding: 3px 6px; width: 100%; box-sizing: border-box;
font-family: monospace; font-size: 12px;`,
btnPrimary: `
background: #1a3a6a; color: #fff; border: 1px solid #3a6aaa;
border-radius: 4px; padding: 6px 18px; cursor: pointer;`,
btnSecondary: `
background: #2e2e2e; color: #ccc; border: 1px solid #555;
border-radius: 4px; padding: 6px 18px; cursor: pointer;`,
btnAdd: `
background: #1e3a1e; color: #8f8; border: 1px solid #3a6a3a;
border-radius: 4px; padding: 5px 14px; cursor: pointer; align-self: flex-start;`,
btnDel: `
background: #5a1a1a; color: #faa; border: 1px solid #8a3a3a;
border-radius: 3px; padding: 2px 7px; cursor: pointer;`,
};
function el(tag, style, attrs = {}) {
const e = document.createElement(tag);
if (style) e.style.cssText = style;
Object.assign(e, attrs);
return e;
}
function makeInput(value, placeholder = "") {
const inp = el("input", STYLE.input, { value, placeholder });
return inp;
}
function makeSelect(selected) {
const sel = el("select", STYLE.input);
for (const opt of ["limited", "all"]) {
const o = document.createElement("option");
o.value = o.textContent = opt;
o.selected = opt === selected;
sel.appendChild(o);
}
return sel;
}
function makeRow(tbody, entry = null) {
const tr = document.createElement("tr");
tr.style.borderBottom = "1px solid #2a2a3a";
const ratioVal = Array.isArray(entry?.ratios)
? entry.ratios.join(", ")
: (entry?.ratios ?? "");
const inputs = {
label: makeInput(entry?.label ?? "", "label"),
words: makeInput((entry?.words ?? []).join(", "), "word1, word2, …"),
ratios: makeInput(ratioVal, "16:9, 21:9 or $LANDSCAPE"),
weights: makeInput((entry?.weights ?? []).join(", "), "3, 2, 1 (optional)"),
category: makeSelect(entry?.category ?? "all"),
};
for (const [key, widget] of Object.entries(inputs)) {
const td = el("td", "padding: 4px;");
widget.dataset.col = key;
td.appendChild(widget);
tr.appendChild(td);
}
const delTd = el("td", "padding: 4px; text-align: center;");
const delBtn = el("button", STYLE.btnDel, { textContent: "✕", title: "Remove row" });
delBtn.addEventListener("click", () => tr.remove());
delTd.appendChild(delBtn);
tr.appendChild(delTd);
tbody.appendChild(tr);
return inputs;
}
function collectRows(tbody) {
return [...tbody.querySelectorAll("tr")].map(tr => {
const get = col => tr.querySelector(`[data-col="${col}"]`)?.value?.trim() ?? "";
const rawRatios = get("ratios");
const ratios = rawRatios.startsWith("$")
? rawRatios
: rawRatios.split(",").map(s => s.trim()).filter(Boolean);
const rawWeights = get("weights");
const weightArr = rawWeights
? rawWeights.split(",").map(s => parseFloat(s.trim())).filter(n => !isNaN(n))
: [];
return {
label: get("label"),
words: get("words").split(",").map(s => s.trim()).filter(Boolean),
ratios,
weights: weightArr.length ? weightArr : null,
category: get("category"),
};
});
}
async function openEditor() {
let keywords;
try {
const resp = await fetch(`${API}/keywords`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
keywords = await resp.json();
} catch (err) {
alert(`Could not load keywords: ${err.message}`);
return;
}
// ── Overlay + dialog ──────────────────────────────────────────────────────
const overlay = el("div", STYLE.overlay);
const dialog = el("div", STYLE.dialog);
overlay.appendChild(dialog);
// Title
const title = el("h3", "margin: 0; color: #a0c4ff; font-size: 15px;",
{ textContent: "Edit Keywords" });
const hint = el("p", "margin: 0; font-size: 11px; color: #777;", {
textContent:
"Limited keywords activate in \u201cLimited\u201d and \u201cAll\u201d modes; All keywords only in \u201cAll\u201d mode. " +
"Use $LANDSCAPE or $PORTRAIT as ratio values to reference the full ratio sets.",
});
// Table
const tableWrap = el("div", "overflow-y: auto; flex: 1; min-height: 0;");
const table = el("table", "width: 100%; border-collapse: collapse;");
const thead = document.createElement("thead");
thead.innerHTML = `<tr style="color:#999; text-align:left; border-bottom:1px solid #444;">
<th style="padding:5px 4px; width:90px;">Label</th>
<th style="padding:5px 4px;">Words (comma-separated)</th>
<th style="padding:5px 4px; width:200px;">Ratios</th>
<th style="padding:5px 4px; width:130px;">Weights</th>
<th style="padding:5px 4px; width:75px;">Category</th>
<th style="padding:5px 4px; width:28px;"></th>
</tr>`;
const tbody = document.createElement("tbody");
table.append(thead, tbody);
tableWrap.appendChild(table);
for (const entry of keywords) makeRow(tbody, entry);
// Add row button
const addBtn = el("button", STYLE.btnAdd, { textContent: "+ Add row" });
addBtn.addEventListener("click", () => makeRow(tbody));
// Footer
const footer = el("div", "display:flex; justify-content:flex-end; gap:8px; align-items:center;");
const status = el("span", "flex:1; font-size:11px; color:#aaa;");
const cancelBtn = el("button", STYLE.btnSecondary, { textContent: "Cancel" });
cancelBtn.addEventListener("click", () => overlay.remove());
const saveBtn = el("button", STYLE.btnPrimary, { textContent: "Save" });
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
status.textContent = "Saving…";
try {
const resp = await fetch(`${API}/keywords`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(collectRows(tbody)),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
status.textContent = "Saved.";
setTimeout(() => overlay.remove(), 700);
} catch (err) {
status.textContent = `Error: ${err.message}`;
saveBtn.disabled = false;
}
});
footer.append(status, cancelBtn, saveBtn);
// Close on backdrop click
overlay.addEventListener("click", e => { if (e.target === overlay) overlay.remove(); });
dialog.append(title, hint, tableWrap, addBtn, footer);
document.body.appendChild(overlay);
}
// ── Register extension ────────────────────────────────────────────────────────
app.registerExtension({
name: "SmartResolution.KeywordsEditor",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "ResolutionNode") return;
const orig = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
orig?.apply(this, arguments);
this.addWidget("button", "Edit Keywords", "edit_keywords", openEditor);
};
},
});