mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-15 23:12:08 +00:00
457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
// Capability rows the active preset uses, as PluginConfig::capabilities_payload emits them:
|
|
// {plugin_key, name, type, type_key, has_config_ui}.
|
|
let capabilities = [];
|
|
|
|
// The selected row's identity; plugin_key is part of it because this list spans plugins.
|
|
let selectedPluginKey = "";
|
|
let selectedCapabilityName = "";
|
|
let selectedCapabilityType = "";
|
|
let selectedHasPresetOverride = false;
|
|
let selectedReadOnly = false;
|
|
|
|
function SafeJsonParse(text) {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function SendWXMessage(message) {
|
|
if (window.wx && typeof window.wx.postMessage === "function")
|
|
window.wx.postMessage(message);
|
|
}
|
|
|
|
function SendMessage(command, payload = {}) {
|
|
const message = {
|
|
sequence_id: Math.round(Date.now() / 1000),
|
|
command: command
|
|
};
|
|
Object.keys(payload).forEach((key) => {
|
|
message[key] = payload[key];
|
|
});
|
|
SendWXMessage(JSON.stringify(message));
|
|
}
|
|
|
|
function HandleStudio(value) {
|
|
const payload = (typeof value === "string") ? SafeJsonParse(value) : value;
|
|
if (!payload || typeof payload !== "object")
|
|
return;
|
|
|
|
if (payload.command === "list_capabilities") {
|
|
ApplyCapabilities(payload);
|
|
} else if (payload.command === "status_message") {
|
|
ShowStatusMessage(String(payload.message || ""), String(payload.level || "info"));
|
|
} else if (payload.command === "capability_config") {
|
|
ApplyCapabilityConfig(payload);
|
|
} else if (payload.command === "capability_config_saved") {
|
|
ApplyCapabilityConfigSaved(payload);
|
|
}
|
|
}
|
|
|
|
function ShowStatusMessage(message, level) {
|
|
const bar = document.getElementById("statusBar");
|
|
const text = document.getElementById("statusText");
|
|
if (!bar || !text)
|
|
return;
|
|
|
|
const normalizedLevel = ["success", "warn", "error", "info"].includes(level) ? level : "info";
|
|
text.textContent = message;
|
|
text.title = message;
|
|
bar.classList.remove("is-empty", "level-success", "level-warn", "level-error", "level-info");
|
|
bar.classList.add(`level-${normalizedLevel}`);
|
|
}
|
|
|
|
function ApplyCapabilities(payload) {
|
|
capabilities = Array.isArray(payload.data) ? payload.data : [];
|
|
|
|
const title = document.getElementById("pageTitle");
|
|
if (title)
|
|
title.textContent = String(payload.title || "Plugin configuration");
|
|
|
|
const subtitle = document.getElementById("pageSubtitle");
|
|
if (subtitle)
|
|
subtitle.textContent = String(payload.preset_name || "");
|
|
|
|
RenderCapabilities();
|
|
}
|
|
|
|
function IsSameCapability(capability) {
|
|
return String(capability.plugin_key || "") === selectedPluginKey
|
|
&& String(capability.name || "") === selectedCapabilityName
|
|
&& String(capability.type_key || "") === selectedCapabilityType;
|
|
}
|
|
|
|
function RenderCapabilities() {
|
|
const empty = document.getElementById("configEmpty");
|
|
const layout = document.getElementById("configLayout");
|
|
const sidebar = document.getElementById("configSidebar");
|
|
if (!empty || !layout || !sidebar)
|
|
return;
|
|
|
|
if (capabilities.length === 0) {
|
|
empty.hidden = false;
|
|
layout.hidden = true;
|
|
sidebar.replaceChildren();
|
|
ClearCapabilityConfigView();
|
|
selectedPluginKey = "";
|
|
selectedCapabilityName = "";
|
|
selectedCapabilityType = "";
|
|
return;
|
|
}
|
|
|
|
empty.hidden = true;
|
|
layout.hidden = false;
|
|
|
|
// Keep the selection across a refresh if the capability is still there, else select the first.
|
|
if (!capabilities.some(IsSameCapability)) {
|
|
selectedPluginKey = String(capabilities[0].plugin_key || "");
|
|
selectedCapabilityName = String(capabilities[0].name || "");
|
|
selectedCapabilityType = String(capabilities[0].type_key || "");
|
|
ClearCapabilityConfigView();
|
|
RequestCapabilityConfig();
|
|
}
|
|
|
|
sidebar.replaceChildren();
|
|
for (const capability of capabilities) {
|
|
const item = document.createElement("button");
|
|
item.type = "button";
|
|
item.className = "config-cap";
|
|
item.dataset.pluginKey = String(capability.plugin_key || "");
|
|
item.dataset.capabilityName = String(capability.name || "");
|
|
item.dataset.capabilityType = String(capability.type_key || "");
|
|
item.setAttribute("role", "option");
|
|
|
|
const isSelected = IsSameCapability(capability);
|
|
item.classList.toggle("selected", isSelected);
|
|
item.setAttribute("aria-selected", isSelected ? "true" : "false");
|
|
|
|
const label = document.createElement("span");
|
|
label.className = "config-cap-name";
|
|
label.textContent = String(capability.name || "");
|
|
item.appendChild(label);
|
|
|
|
const type = document.createElement("span");
|
|
type.className = "config-cap-type";
|
|
type.textContent = String(capability.type || "");
|
|
item.appendChild(type);
|
|
|
|
sidebar.appendChild(item);
|
|
}
|
|
}
|
|
|
|
function OnConfigSidebarClick(event) {
|
|
const item = event.target.closest(".config-cap");
|
|
if (!item)
|
|
return;
|
|
|
|
const pluginKey = String(item.dataset.pluginKey || "");
|
|
const name = String(item.dataset.capabilityName || "");
|
|
const typeKey = String(item.dataset.capabilityType || "");
|
|
if (!name || (pluginKey === selectedPluginKey && name === selectedCapabilityName && typeKey === selectedCapabilityType))
|
|
return;
|
|
|
|
selectedPluginKey = pluginKey;
|
|
selectedCapabilityName = name;
|
|
selectedCapabilityType = typeKey;
|
|
|
|
// The native reply is async: clear now so the old config cannot appear under the new selection.
|
|
ClearCapabilityConfigView();
|
|
RequestCapabilityConfig();
|
|
RenderCapabilities();
|
|
}
|
|
|
|
// Config editor: the host's JSON editor, or the capability's own HTML UI in a sandboxed frame.
|
|
// Both edit the same stored config; the page renders what the native side sends.
|
|
|
|
// Replies are async: apply one only if it still matches the selected row (plugin_key included,
|
|
// since this list spans plugins), so a stale reply never lands under another capability.
|
|
function IsCurrentCapability(payload) {
|
|
return String(payload?.plugin_key || "") === selectedPluginKey
|
|
&& String(payload?.capability_name || "") === selectedCapabilityName
|
|
&& String(payload?.capability_type || "") === selectedCapabilityType;
|
|
}
|
|
|
|
function RequestCapabilityConfig() {
|
|
if (!selectedPluginKey || !selectedCapabilityName)
|
|
return;
|
|
|
|
SendMessage("get_capability_config", {
|
|
plugin_key: selectedPluginKey,
|
|
capability_name: selectedCapabilityName,
|
|
capability_type: selectedCapabilityType
|
|
});
|
|
}
|
|
|
|
// Empties both editors and the footer, so nothing from the previous capability lingers while the
|
|
// next one is in flight.
|
|
function ClearCapabilityConfigView() {
|
|
const editor = document.getElementById("configEditor");
|
|
const custom = document.getElementById("configCustom");
|
|
const text = document.getElementById("configText");
|
|
const error = document.getElementById("configError");
|
|
const footer = document.getElementById("configFooter");
|
|
const meta = document.getElementById("configMeta");
|
|
|
|
if (editor)
|
|
editor.hidden = true;
|
|
if (custom) {
|
|
custom.hidden = true;
|
|
custom.removeAttribute("srcdoc");
|
|
}
|
|
if (text)
|
|
text.value = "";
|
|
if (error) {
|
|
error.hidden = true;
|
|
error.textContent = "";
|
|
}
|
|
if (footer)
|
|
footer.hidden = true;
|
|
if (meta)
|
|
meta.hidden = true;
|
|
selectedHasPresetOverride = false;
|
|
selectedReadOnly = false;
|
|
SetConfigValidation("");
|
|
}
|
|
|
|
function UpdateConfigMeta(payload) {
|
|
selectedHasPresetOverride = payload?.has_preset_override === true;
|
|
selectedReadOnly = payload?.read_only === true;
|
|
|
|
const meta = document.getElementById("configMeta");
|
|
const badge = document.getElementById("configSourceBadge");
|
|
const save = document.getElementById("configSaveBtn");
|
|
const restore = document.getElementById("configRestoreBtn");
|
|
if (!meta || !badge)
|
|
return;
|
|
|
|
const source = String(payload?.source || "none");
|
|
badge.textContent = source === "preset" ? "Preset override" :
|
|
(source === "base" ? "Inherited from global configuration" : "No saved configuration");
|
|
if (save)
|
|
save.disabled = selectedReadOnly;
|
|
if (restore)
|
|
restore.disabled = selectedReadOnly || !selectedHasPresetOverride;
|
|
meta.hidden = false;
|
|
}
|
|
|
|
function ApplyCapabilityConfig(payload) {
|
|
if (!IsCurrentCapability(payload))
|
|
return;
|
|
|
|
const editor = document.getElementById("configEditor");
|
|
const custom = document.getElementById("configCustom");
|
|
const text = document.getElementById("configText");
|
|
const error = document.getElementById("configError");
|
|
|
|
const message = String(payload?.error || "");
|
|
if (error) {
|
|
error.textContent = message;
|
|
error.hidden = message === "";
|
|
}
|
|
|
|
const config = payload && Object.prototype.hasOwnProperty.call(payload, "config") ? payload.config : {};
|
|
const html = String(payload?.custom_html || "");
|
|
UpdateConfigMeta(payload);
|
|
|
|
// Restore applies to either editor; Save and validation belong to the JSON editor only.
|
|
ShowConfigFooter(!html);
|
|
|
|
if (html) {
|
|
if (custom) {
|
|
custom.hidden = false;
|
|
custom.srcdoc = BuildCustomConfigDocument(html, config);
|
|
}
|
|
if (editor)
|
|
editor.hidden = true;
|
|
return;
|
|
}
|
|
|
|
// Default editor: any reason a custom UI is unavailable already arrived in payload.error.
|
|
if (custom) {
|
|
custom.hidden = true;
|
|
custom.removeAttribute("srcdoc");
|
|
}
|
|
if (editor)
|
|
editor.hidden = false;
|
|
if (text)
|
|
text.value = JSON.stringify(config, null, 2);
|
|
SetConfigValidation("");
|
|
}
|
|
|
|
// withEditorControls is false for a custom UI, which saves through its own controls.
|
|
function ShowConfigFooter(withEditorControls) {
|
|
const footer = document.getElementById("configFooter");
|
|
const save = document.getElementById("configSaveBtn");
|
|
const validation = document.getElementById("configValidation");
|
|
|
|
if (footer)
|
|
footer.hidden = false;
|
|
if (save)
|
|
save.hidden = !withEditorControls;
|
|
if (validation)
|
|
validation.hidden = !withEditorControls;
|
|
}
|
|
|
|
function SetConfigValidation(message) {
|
|
const node = document.getElementById("configValidation");
|
|
const save = document.getElementById("configSaveBtn");
|
|
if (node) {
|
|
node.textContent = message;
|
|
node.classList.toggle("invalid", message !== "");
|
|
}
|
|
// Invalid JSON is never saved: Save is the only way to persist. The native side re-validates.
|
|
if (save)
|
|
save.disabled = selectedReadOnly || message !== "";
|
|
}
|
|
|
|
function ValidateConfigText() {
|
|
const text = document.getElementById("configText");
|
|
if (!text)
|
|
return false;
|
|
|
|
try {
|
|
JSON.parse(text.value);
|
|
SetConfigValidation("");
|
|
return true;
|
|
} catch (err) {
|
|
SetConfigValidation(String(err?.message || "Invalid JSON"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function SaveCapabilityConfig() {
|
|
if (!selectedPluginKey || !selectedCapabilityName)
|
|
return;
|
|
|
|
const text = document.getElementById("configText");
|
|
if (!text)
|
|
return;
|
|
|
|
if (!ValidateConfigText())
|
|
return;
|
|
|
|
// Sent as text on purpose: the native side is the authority on validity and parses it itself.
|
|
SendMessage("save_capability_config", {
|
|
plugin_key: selectedPluginKey,
|
|
capability_name: selectedCapabilityName,
|
|
capability_type: selectedCapabilityType,
|
|
config: text.value
|
|
});
|
|
}
|
|
|
|
// "Restore defaults" here drops the preset's override, so the capability falls back to the global
|
|
// configuration. The native side confirms, then re-sends the config that is now effective.
|
|
function RestoreCapabilityConfig() {
|
|
if (!selectedPluginKey || !selectedCapabilityName || !selectedHasPresetOverride)
|
|
return;
|
|
|
|
SendMessage("remove_preset_override", {
|
|
plugin_key: selectedPluginKey,
|
|
capability_name: selectedCapabilityName,
|
|
capability_type: selectedCapabilityType
|
|
});
|
|
}
|
|
|
|
function ApplyCapabilityConfigSaved(payload) {
|
|
if (!IsCurrentCapability(payload))
|
|
return;
|
|
|
|
const error = document.getElementById("configError");
|
|
const message = String(payload?.error || "");
|
|
if (error) {
|
|
error.textContent = message;
|
|
error.hidden = message === "";
|
|
}
|
|
if (payload?.ok !== true)
|
|
return;
|
|
|
|
// Reload from what was persisted, not from what was typed.
|
|
const config = payload && Object.prototype.hasOwnProperty.call(payload, "config") ? payload.config : {};
|
|
const custom = document.getElementById("configCustom");
|
|
const text = document.getElementById("configText");
|
|
|
|
if (custom && !custom.hidden && custom.contentWindow)
|
|
custom.contentWindow.postMessage({ __orca: "config", config: config }, "*");
|
|
else if (text)
|
|
text.value = JSON.stringify(config, null, 2);
|
|
|
|
SetConfigValidation("");
|
|
}
|
|
|
|
// The whole host surface a custom config UI gets: read the config, save one, be told when a save
|
|
// lands. The frame is sandboxed into an opaque origin, so this bridge is its only channel.
|
|
function BuildCustomConfigDocument(html, config) {
|
|
// Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the
|
|
// literal stays valid JSON.
|
|
const seed = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
const bridge = `<script>
|
|
(function () {
|
|
var handlers = [];
|
|
var current = ${seed};
|
|
window.orca = {
|
|
getConfig: function () { return current; },
|
|
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
|
|
onConfig: function (cb) {
|
|
if (typeof cb !== "function") return;
|
|
handlers.push(cb);
|
|
try { cb(current); } catch (e) {}
|
|
}
|
|
};
|
|
window.addEventListener("message", function (event) {
|
|
if (!event.data || event.data.__orca !== "config") return;
|
|
current = event.data.config || {};
|
|
handlers.forEach(function (handler) {
|
|
try { handler(current); } catch (e) {}
|
|
});
|
|
});
|
|
})();
|
|
<\/script>`;
|
|
return bridge + html;
|
|
}
|
|
|
|
function OnCustomConfigMessage(event) {
|
|
const custom = document.getElementById("configCustom");
|
|
// Only the frame we created, and only while it is actually showing.
|
|
if (!custom || custom.hidden || !custom.contentWindow || event.source !== custom.contentWindow)
|
|
return;
|
|
|
|
const data = event.data;
|
|
if (!data || data.__orca !== "save")
|
|
return;
|
|
if (!selectedPluginKey || !selectedCapabilityName)
|
|
return;
|
|
|
|
SendMessage("save_capability_config", {
|
|
plugin_key: selectedPluginKey,
|
|
capability_name: selectedCapabilityName,
|
|
capability_type: selectedCapabilityType,
|
|
config: data.config === undefined ? {} : data.config
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const sidebar = document.getElementById("configSidebar");
|
|
if (sidebar)
|
|
sidebar.addEventListener("click", OnConfigSidebarClick);
|
|
|
|
const saveBtn = document.getElementById("configSaveBtn");
|
|
if (saveBtn)
|
|
saveBtn.addEventListener("click", SaveCapabilityConfig);
|
|
|
|
const restoreBtn = document.getElementById("configRestoreBtn");
|
|
if (restoreBtn)
|
|
restoreBtn.addEventListener("click", RestoreCapabilityConfig);
|
|
|
|
const text = document.getElementById("configText");
|
|
if (text)
|
|
text.addEventListener("input", ValidateConfigText);
|
|
|
|
// The custom UI is sandboxed into an opaque origin, so postMessage is its only channel.
|
|
// OnCustomConfigMessage matches on the frame's contentWindow, not the origin ("null" when
|
|
// sandboxed), and ignores anything else.
|
|
window.addEventListener("message", OnCustomConfigMessage);
|
|
|
|
SendMessage("request_capabilities");
|
|
});
|