chore: UI Updates

This commit is contained in:
Ian Chua
2026-07-14 14:42:32 +08:00
parent 8cc4eaea1e
commit 06cd8ad0d5
10 changed files with 67 additions and 141 deletions

View File

@@ -13,8 +13,7 @@
<body> <body>
<main class="page"> <main class="page">
<header class="page-header"> <header class="page-header">
<h1 id="pageTitle" class="page-title">Plugin configuration</h1> <h1 id="pagePresetName" class="page-title"></h1>
<p id="pageSubtitle" class="page-subtitle"></p>
</header> </header>
<div id="configEmpty" class="detail-empty">This preset does not use any plugin capabilities</div> <div id="configEmpty" class="detail-empty">This preset does not use any plugin capabilities</div>
@@ -24,9 +23,6 @@
aria-label="Capabilities used by this preset"></div> aria-label="Capabilities used by this preset"></div>
<div class="config-view"> <div class="config-view">
<div id="configError" class="config-error" role="status" aria-live="polite" hidden></div> <div id="configError" class="config-error" role="status" aria-live="polite" hidden></div>
<div id="configMeta" class="config-meta" hidden>
<span id="configSourceBadge" class="config-source-badge"></span>
</div>
<div id="configEditor" class="config-editor" hidden> <div id="configEditor" class="config-editor" hidden>
<textarea id="configText" class="config-textarea thin-scroll" spellcheck="false" <textarea id="configText" class="config-textarea thin-scroll" spellcheck="false"
autocomplete="off" autocapitalize="off" aria-label="Capability configuration (JSON)"></textarea> autocomplete="off" autocapitalize="off" aria-label="Capability configuration (JSON)"></textarea>

View File

@@ -65,13 +65,9 @@ function ShowStatusMessage(message, level) {
function ApplyCapabilities(payload) { function ApplyCapabilities(payload) {
capabilities = Array.isArray(payload.data) ? payload.data : []; capabilities = Array.isArray(payload.data) ? payload.data : [];
const title = document.getElementById("pageTitle"); const presetName = document.getElementById("pagePresetName");
if (title) if (presetName)
title.textContent = String(payload.title || "Plugin configuration"); presetName.textContent = String(payload.preset_name || "");
const subtitle = document.getElementById("pageSubtitle");
if (subtitle)
subtitle.textContent = String(payload.preset_name || "");
RenderCapabilities(); RenderCapabilities();
} }
@@ -191,7 +187,6 @@ function ClearCapabilityConfigView() {
const text = document.getElementById("configText"); const text = document.getElementById("configText");
const error = document.getElementById("configError"); const error = document.getElementById("configError");
const footer = document.getElementById("configFooter"); const footer = document.getElementById("configFooter");
const meta = document.getElementById("configMeta");
if (editor) if (editor)
editor.hidden = true; editor.hidden = true;
@@ -207,32 +202,23 @@ function ClearCapabilityConfigView() {
} }
if (footer) if (footer)
footer.hidden = true; footer.hidden = true;
if (meta)
meta.hidden = true;
selectedHasPresetOverride = false; selectedHasPresetOverride = false;
selectedReadOnly = false; selectedReadOnly = false;
SetConfigValidation(""); SetConfigValidation("");
} }
function UpdateConfigMeta(payload) { // A read-only capability cannot be saved, and there is nothing to restore until the preset overrides
// the global configuration.
function UpdateConfigActions(payload) {
selectedHasPresetOverride = payload?.has_preset_override === true; selectedHasPresetOverride = payload?.has_preset_override === true;
selectedReadOnly = payload?.read_only === true; selectedReadOnly = payload?.read_only === true;
const meta = document.getElementById("configMeta");
const badge = document.getElementById("configSourceBadge");
const save = document.getElementById("configSaveBtn"); const save = document.getElementById("configSaveBtn");
const restore = document.getElementById("configRestoreBtn"); 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) if (save)
save.disabled = selectedReadOnly; save.disabled = selectedReadOnly;
if (restore) if (restore)
restore.disabled = selectedReadOnly || !selectedHasPresetOverride; restore.disabled = selectedReadOnly || !selectedHasPresetOverride;
meta.hidden = false;
} }
function ApplyCapabilityConfig(payload) { function ApplyCapabilityConfig(payload) {
@@ -252,10 +238,13 @@ function ApplyCapabilityConfig(payload) {
const config = payload && Object.prototype.hasOwnProperty.call(payload, "config") ? payload.config : {}; const config = payload && Object.prototype.hasOwnProperty.call(payload, "config") ? payload.config : {};
const html = String(payload?.custom_html || ""); const html = String(payload?.custom_html || "");
UpdateConfigMeta(payload); UpdateConfigActions(payload);
// Restore applies to either editor; Save and validation belong to the JSON editor only. // The footer belongs to the JSON editor. A custom UI owns its whole surface, including whatever
ShowConfigFooter(!html); // save/restore controls it wants, and reaches the host through the window.orca bridge.
const footer = document.getElementById("configFooter");
if (footer)
footer.hidden = html !== "";
if (html) { if (html) {
if (custom) { if (custom) {
@@ -279,20 +268,6 @@ function ApplyCapabilityConfig(payload) {
SetConfigValidation(""); 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) { function SetConfigValidation(message) {
const node = document.getElementById("configValidation"); const node = document.getElementById("configValidation");
const save = document.getElementById("configSaveBtn"); const save = document.getElementById("configSaveBtn");
@@ -379,8 +354,9 @@ function ApplyCapabilityConfigSaved(payload) {
SetConfigValidation(""); SetConfigValidation("");
} }
// The whole host surface a custom config UI gets: read the config, save one, be told when a save // The whole host surface a custom config UI gets: read the config, save one, drop the preset's
// lands. The frame is sandboxed into an opaque origin, so this bridge is its only channel. // override, and be told when either lands. The frame is sandboxed into an opaque origin, so this
// bridge is its only channel.
function BuildCustomConfigDocument(html, config) { function BuildCustomConfigDocument(html, config) {
// Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the // Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the
// literal stays valid JSON. // literal stays valid JSON.
@@ -392,6 +368,7 @@ function BuildCustomConfigDocument(html, config) {
window.orca = { window.orca = {
getConfig: function () { return current; }, getConfig: function () { return current; },
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); }, saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); },
onConfig: function (cb) { onConfig: function (cb) {
if (typeof cb !== "function") return; if (typeof cb !== "function") return;
handlers.push(cb); handlers.push(cb);
@@ -417,17 +394,21 @@ function OnCustomConfigMessage(event) {
return; return;
const data = event.data; const data = event.data;
if (!data || data.__orca !== "save") if (!data || !selectedPluginKey || !selectedCapabilityName)
return;
if (!selectedPluginKey || !selectedCapabilityName)
return; return;
SendMessage("save_capability_config", { if (data.__orca === "save") {
plugin_key: selectedPluginKey, SendMessage("save_capability_config", {
capability_name: selectedCapabilityName, plugin_key: selectedPluginKey,
capability_type: selectedCapabilityType, capability_name: selectedCapabilityName,
config: data.config === undefined ? {} : data.config capability_type: selectedCapabilityType,
}); config: data.config === undefined ? {} : data.config
});
return;
}
if (data.__orca === "restore")
RestoreCapabilityConfig();
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@@ -40,12 +40,6 @@ body {
font-weight: 600; font-weight: 600;
} }
.page-subtitle {
margin: 4px 0 0;
font-size: 12px;
opacity: 0.7;
}
/* ---- Ported from resources/web/dialog/PluginsDialog/styles.css (Config tab) ---- */ /* ---- Ported from resources/web/dialog/PluginsDialog/styles.css (Config tab) ---- */
.detail-empty { .detail-empty {
@@ -138,27 +132,6 @@ body {
display: none; display: none;
} }
.config-meta {
display: flex;
align-items: center;
gap: 8px;
}
.config-meta[hidden] {
display: none;
}
.config-source-badge {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 10px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
font-size: 11px;
}
.config-editor { .config-editor {
display: flex; display: flex;
flex: 1; flex: 1;

View File

@@ -157,8 +157,8 @@
window.orca bridge. --> window.orca bridge. -->
<iframe id="configCustom" class="config-custom" title="Plugin configuration" <iframe id="configCustom" class="config-custom" title="Plugin configuration"
sandbox="allow-scripts" referrerpolicy="no-referrer" hidden></iframe> sandbox="allow-scripts" referrerpolicy="no-referrer" hidden></iframe>
<!-- Host chrome for both editors: a custom UI needs Restore too, and keeping it here <!-- JSON-editor chrome only: a custom UI renders its own save/restore controls and
leaves it off the JS bridge. Save and validation are JSON-editor only. --> drives them through the window.orca bridge. -->
<div id="configFooter" class="config-view-footer" hidden> <div id="configFooter" class="config-view-footer" hidden>
<span id="configValidation" class="config-validation" role="status" aria-live="polite"></span> <span id="configValidation" class="config-validation" role="status" aria-live="polite"></span>
<div class="config-actions"> <div class="config-actions">

View File

@@ -975,8 +975,11 @@ function ApplyCapabilityConfig(payload) {
const config = (payload && typeof payload.config === "object" && payload.config !== null) ? payload.config : {}; const config = (payload && typeof payload.config === "object" && payload.config !== null) ? payload.config : {};
const html = String(payload?.custom_html || ""); const html = String(payload?.custom_html || "");
// Restore applies to either editor; Save and validation belong to the JSON editor only. // The footer belongs to the JSON editor. A custom UI owns its whole surface, including whatever
ShowConfigFooter(!html); // save/restore controls it wants, and reaches the host through the window.orca bridge.
const footer = document.getElementById("configFooter");
if (footer)
footer.hidden = html !== "";
if (html) { if (html) {
if (custom) { if (custom) {
@@ -1000,20 +1003,6 @@ function ApplyCapabilityConfig(payload) {
SetConfigValidation(""); 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) { function SetConfigValidation(message) {
const node = document.getElementById("configValidation"); const node = document.getElementById("configValidation");
const save = document.getElementById("configSaveBtn"); const save = document.getElementById("configSaveBtn");
@@ -1096,8 +1085,9 @@ function ApplyCapabilityConfigSaved(payload) {
SetConfigValidation(""); SetConfigValidation("");
} }
// The whole host surface a custom config UI gets: read the config, save one, be told when a save // The whole host surface a custom config UI gets: read the config, save one, restore the plugin's
// lands. The frame is sandboxed into an opaque origin, so this bridge is its only channel. // defaults, and be told when either lands. The frame is sandboxed into an opaque origin, so this
// bridge is its only channel.
function BuildCustomConfigDocument(html, config) { function BuildCustomConfigDocument(html, config) {
// Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the // Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the
// literal stays valid JSON. // literal stays valid JSON.
@@ -1109,6 +1099,7 @@ function BuildCustomConfigDocument(html, config) {
window.orca = { window.orca = {
getConfig: function () { return current; }, getConfig: function () { return current; },
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); }, saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); },
onConfig: function (cb) { onConfig: function (cb) {
if (typeof cb !== "function") return; if (typeof cb !== "function") return;
handlers.push(cb); handlers.push(cb);
@@ -1134,17 +1125,21 @@ function OnCustomConfigMessage(event) {
return; return;
const data = event.data; const data = event.data;
if (!data || data.__orca !== "save") if (!data || !selectedPluginId || !selectedCapabilityName)
return;
if (!selectedPluginId || !selectedCapabilityName)
return; return;
SendMessage("save_capability_config", { if (data.__orca === "save") {
plugin_key: selectedPluginId, SendMessage("save_capability_config", {
capability_name: selectedCapabilityName, plugin_key: selectedPluginId,
capability_type: selectedCapabilityType, capability_name: selectedCapabilityName,
config: data.config === undefined ? {} : data.config capability_type: selectedCapabilityType,
}); config: data.config === undefined ? {} : data.config
});
return;
}
if (data.__orca === "restore")
RestoreCapabilityConfig();
} }
function RenderThumbnail(plugin) { function RenderThumbnail(plugin) {

View File

@@ -136,7 +136,6 @@ void PluginsConfigDialog::send_capabilities()
nlohmann::json response; nlohmann::json response;
response["command"] = "list_capabilities"; response["command"] = "list_capabilities";
response["preset_type"] = static_cast<int>(m_type); response["preset_type"] = static_cast<int>(m_type);
response["title"] = into_u8(preset_type_title(m_type));
response["preset_name"] = preset->name; response["preset_name"] = preset->name;
response["data"] = PluginConfig::capabilities_payload(capabilities_in_use(m_type, *preset)); response["data"] = PluginConfig::capabilities_payload(capabilities_in_use(m_type, *preset));
@@ -166,7 +165,6 @@ void PluginsConfigDialog::send_capability_config(const PluginCapabilityIdentifie
const EffectiveCapabilityConfig effective = m_service.get_effective_config(m_overrides, id); const EffectiveCapabilityConfig effective = m_service.get_effective_config(m_overrides, id);
response["config"] = effective.config; response["config"] = effective.config;
response["source"] = plugin_config_source_to_string(effective.source);
response["has_preset_override"] = effective.has_preset_override; response["has_preset_override"] = effective.has_preset_override;
response["has_base_config"] = effective.has_base_config; response["has_base_config"] = effective.has_base_config;
response["stored_plugin_version"] = effective.stored_plugin_version; response["stored_plugin_version"] = effective.stored_plugin_version;

View File

@@ -3094,6 +3094,17 @@ static bool cloud_media_is_image(const nlohmann::json& main_image)
return lowered(get_json_string_field(main_image, "media_type")) == "image"; return lowered(get_json_string_field(main_image, "media_type")) == "image";
} }
// creator_display_name degrades to the creator's username and then to their raw user id when the
// cloud cannot resolve a profile, so a bare id is dropped rather than shown: the author then falls
// back to the one the plugin's own manifest declares (see apply_plugin_metadata_fallbacks).
static std::string parse_cloud_author(const nlohmann::json& item)
{
std::string author = get_json_string_field(item, "creator_display_name");
if (author.empty() || is_uuid(author))
author = get_json_string_field(item, "creator_username");
return is_uuid(author) ? std::string() : author;
}
int OrcaCloudServiceAgent::fetch_subscribed_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors, int OrcaCloudServiceAgent::fetch_subscribed_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
std::vector<std::string>& not_found, std::vector<std::string>& not_found,
std::vector<std::string>& unauthorized) std::vector<std::string>& unauthorized)
@@ -3137,11 +3148,7 @@ int OrcaCloudServiceAgent::fetch_subscribed_manifests_into_descriptors(std::vect
descriptor.plugin_key = uuid; descriptor.plugin_key = uuid;
// Cloud API "description" is intentionally not parsed: descriptions come only from the // Cloud API "description" is intentionally not parsed: descriptions come only from the
// plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link. // plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link.
descriptor.author = get_json_string_field(item, "author"); descriptor.author = parse_cloud_author(item);
if (descriptor.author.empty())
descriptor.author = get_json_string_field(item, "creator_display_name");
if (descriptor.author.empty())
descriptor.author = get_json_string_field(item, "creator_username");
descriptor.version = get_json_string_field(item, "version"); descriptor.version = get_json_string_field(item, "version");
descriptor.latest_version = descriptor.version; descriptor.latest_version = descriptor.version;
// Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are // Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are
@@ -3239,11 +3246,7 @@ int OrcaCloudServiceAgent::fetch_mine_manifests_into_descriptors(std::vector<Plu
descriptor.plugin_key = uuid; descriptor.plugin_key = uuid;
// Cloud API "description" is intentionally not parsed: descriptions come only from the // Cloud API "description" is intentionally not parsed: descriptions come only from the
// plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link. // plugin's Python header once installed. Cloud-only rows show a "View on OrcaCloud" link.
descriptor.author = get_json_string_field(item, "author"); descriptor.author = parse_cloud_author(item);
if (descriptor.author.empty())
descriptor.author = get_json_string_field(item, "creator_display_name");
if (descriptor.author.empty())
descriptor.author = get_json_string_field(item, "creator_username");
descriptor.version = get_json_string_field(item, "version"); descriptor.version = get_json_string_field(item, "version");
descriptor.latest_version = descriptor.version; descriptor.latest_version = descriptor.version;
// Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are // Cloud "type" is cosmetic (see parse_cloud_display_types); real capability_types are

View File

@@ -18,12 +18,6 @@ namespace Slic3r {
class PluginCapabilityInterface; class PluginCapabilityInterface;
enum class PluginConfigSource {
None,
Base,
Preset,
};
/* /*
Example config.json shape Example config.json shape
{ {

View File

@@ -91,15 +91,6 @@ std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
return document.empty() ? std::string() : document.serialize_entries().dump(); return document.empty() ? std::string() : document.serialize_entries().dump();
} }
std::string plugin_config_source_to_string(PluginConfigSource source)
{
switch (source) {
case PluginConfigSource::Preset: return "preset";
case PluginConfigSource::Base: return "base";
default: return "none";
}
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides, EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const const PluginCapabilityIdentifier& id) const
{ {
@@ -112,14 +103,12 @@ EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const
if (const auto entry = overrides.find(result.id)) { if (const auto entry = overrides.find(result.id)) {
result.has_preset_override = true; result.has_preset_override = true;
result.source = PluginConfigSource::Preset;
result.config = entry->cap_config; result.config = entry->cap_config;
result.stored_plugin_version = entry->plugin_version; result.stored_plugin_version = entry->plugin_version;
return result; return result;
} }
if (result.has_base_config) { if (result.has_base_config) {
result.source = PluginConfigSource::Base;
result.config = base.config; result.config = base.config;
result.stored_plugin_version = base.plugin_version; result.stored_plugin_version = base.plugin_version;
} }

View File

@@ -30,7 +30,6 @@ struct EffectiveCapabilityConfig
{ {
CapabilityConfigId id; CapabilityConfigId id;
nlohmann::json config = nlohmann::json::object(); nlohmann::json config = nlohmann::json::object();
PluginConfigSource source = PluginConfigSource::None;
bool has_preset_override = false; bool has_preset_override = false;
bool has_base_config = false; bool has_base_config = false;
@@ -46,8 +45,6 @@ struct MutationResult
EffectiveCapabilityConfig effective; EffectiveCapabilityConfig effective;
}; };
std::string plugin_config_source_to_string(PluginConfigSource source);
// Resolves a capability's effective config as `preset override -> base config -> none`, and mutates // Resolves a capability's effective config as `preset override -> base config -> none`, and mutates
// the override layer. It works on a CapabilityConfigDocument the caller owns, never on a Preset and // the override layer. It works on a CapabilityConfigDocument the caller owns, never on a Preset and
// never on the base config file, which keeps the two layers from writing to each other: // never on the base config file, which keeps the two layers from writing to each other: