mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-22 02:12:13 +00:00
Compare commits
2 Commits
fix/crash_
...
fix/cloud-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b397a8f6 | ||
|
|
3dae6f45e8 |
@@ -601,15 +601,17 @@ function SourceLabel(source) {
|
|||||||
return "Mine";
|
return "Mine";
|
||||||
case "subscribed":
|
case "subscribed":
|
||||||
return "Subscribed";
|
return "Subscribed";
|
||||||
|
case "orphaned":
|
||||||
|
return "Orphaned";
|
||||||
default:
|
default:
|
||||||
return "Local";
|
return "Local";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared Local/Subscribed/Mine pill, used both after the row name and in the info panel.
|
// Shared source pill, used both after the row name and in the info panel.
|
||||||
function SourceBadge(source) {
|
function SourceBadge(source) {
|
||||||
const normalized = String(source || "").toLowerCase();
|
const normalized = String(source || "").toLowerCase();
|
||||||
const variant = (normalized === "mine" || normalized === "subscribed") ? normalized : "local";
|
const variant = (normalized === "mine" || normalized === "subscribed" || normalized === "orphaned") ? normalized : "local";
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = `plugin-source-badge source-${variant}`;
|
badge.className = `plugin-source-badge source-${variant}`;
|
||||||
badge.textContent = SourceLabel(source);
|
badge.textContent = SourceLabel(source);
|
||||||
@@ -653,7 +655,7 @@ function LabelCell(plugin, isExpanded = false, capabilityCount = 0, nameRanges =
|
|||||||
const labelCell = document.createElement("span");
|
const labelCell = document.createElement("span");
|
||||||
labelCell.className = "label-cell";
|
labelCell.className = "label-cell";
|
||||||
|
|
||||||
const hasCloudLink = plugin.source === "mine" || plugin.source === "subscribed";
|
const hasCloudLink = plugin.source === "mine" || plugin.source === "subscribed" || plugin.source === "orphaned";
|
||||||
const pluginLabelText = plugin.label || plugin.name || plugin.plugin_id || "";
|
const pluginLabelText = plugin.label || plugin.name || plugin.plugin_id || "";
|
||||||
const canExpand = capabilityCount > 0;
|
const canExpand = capabilityCount > 0;
|
||||||
|
|
||||||
@@ -704,7 +706,7 @@ function LabelCell(plugin, isExpanded = false, capabilityCount = 0, nameRanges =
|
|||||||
function SourceCell(plugin) {
|
function SourceCell(plugin) {
|
||||||
const cell = document.createElement("span");
|
const cell = document.createElement("span");
|
||||||
const normalized = String(plugin.source || "").toLowerCase();
|
const normalized = String(plugin.source || "").toLowerCase();
|
||||||
const variant = (normalized === "mine" || normalized === "subscribed") ? normalized : "local";
|
const variant = (normalized === "mine" || normalized === "subscribed" || normalized === "orphaned") ? normalized : "local";
|
||||||
cell.className = `source-cell source-${variant}`;
|
cell.className = `source-cell source-${variant}`;
|
||||||
|
|
||||||
const sourceLabel = document.createElement("span");
|
const sourceLabel = document.createElement("span");
|
||||||
@@ -1265,7 +1267,7 @@ function RenderDescription(plugin) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCloud = plugin && (plugin.source === "mine" || plugin.source === "subscribed");
|
const isCloud = plugin && (plugin.source === "mine" || plugin.source === "subscribed" || plugin.source === "orphaned");
|
||||||
if (isCloud && String(plugin?.sharing_token || "")) {
|
if (isCloud && String(plugin?.sharing_token || "")) {
|
||||||
node.appendChild(document.createTextNode("View on OrcaCloud "));
|
node.appendChild(document.createTextNode("View on OrcaCloud "));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@@ -1402,6 +1404,13 @@ function RenderDetailSummary(container, plugin) {
|
|||||||
message.textContent = errorText || StatusDescription(plugin);
|
message.textContent = errorText || StatusDescription(plugin);
|
||||||
container.appendChild(message);
|
container.appendChild(message);
|
||||||
|
|
||||||
|
if (plugin.orphaned === true) {
|
||||||
|
const warning = document.createElement("div");
|
||||||
|
warning.className = "detail-description detail-warning-text";
|
||||||
|
warning.textContent = "Orphaned: This plugin is no longer subscribed or available in OrcaCloud. The local copy remains installed and can still be used.";
|
||||||
|
container.appendChild(warning);
|
||||||
|
}
|
||||||
|
|
||||||
const updateStatus = GetUpdateStatus(plugin);
|
const updateStatus = GetUpdateStatus(plugin);
|
||||||
if (updateStatus === "update_available") {
|
if (updateStatus === "update_available") {
|
||||||
const note = document.createElement("div");
|
const note = document.createElement("div");
|
||||||
|
|||||||
@@ -251,6 +251,11 @@ body.pane-resizing {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-cell.source-orphaned {
|
||||||
|
color: var(--plugin-status-warn);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.source-cell.source-local {
|
.source-cell.source-local {
|
||||||
color: var(--plugin-source-neutral-text);
|
color: var(--plugin-source-neutral-text);
|
||||||
}
|
}
|
||||||
@@ -648,6 +653,10 @@ body.pane-resizing {
|
|||||||
color: var(--plugin-status-danger);
|
color: var(--plugin-status-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-warning-text {
|
||||||
|
color: var(--plugin-status-warn);
|
||||||
|
}
|
||||||
|
|
||||||
.detail-status-chip {
|
.detail-status-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -915,6 +924,11 @@ body.pane-resizing {
|
|||||||
color: var(--plugin-source-subscribed-text);
|
color: var(--plugin-source-subscribed-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-source-badge.source-orphaned {
|
||||||
|
background: var(--plugin-status-warn-bg);
|
||||||
|
color: var(--plugin-status-warn);
|
||||||
|
}
|
||||||
|
|
||||||
.plugin-cloud-link {
|
.plugin-cloud-link {
|
||||||
color: var(--plugin-link-text);
|
color: var(--plugin-link-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Slic3r
|
|||||||
// IMPORTANT: ordinal order is the Plugins dialog Source sort priority.
|
// IMPORTANT: ordinal order is the Plugins dialog Source sort priority.
|
||||||
Mine,
|
Mine,
|
||||||
Subscribed,
|
Subscribed,
|
||||||
|
Orphaned,
|
||||||
Local
|
Local
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ namespace Slic3r
|
|||||||
{
|
{
|
||||||
case PluginSource::Mine: return "mine";
|
case PluginSource::Mine: return "mine";
|
||||||
case PluginSource::Subscribed: return "subscribed";
|
case PluginSource::Subscribed: return "subscribed";
|
||||||
|
case PluginSource::Orphaned: return "orphaned";
|
||||||
case PluginSource::Local: return "local";
|
case PluginSource::Local: return "local";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ struct PluginDialogItem
|
|||||||
bool loading = false;
|
bool loading = false;
|
||||||
|
|
||||||
bool is_cloud_plugin = false;
|
bool is_cloud_plugin = false;
|
||||||
|
bool orphaned = false;
|
||||||
bool has_local_package = false;
|
bool has_local_package = false;
|
||||||
bool unauthorized = false;
|
bool unauthorized = false;
|
||||||
bool has_script_capability = false;
|
bool has_script_capability = false;
|
||||||
@@ -243,6 +244,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
|
|||||||
payload_item["sharing_token"] = dialog_item.sharing_token;
|
payload_item["sharing_token"] = dialog_item.sharing_token;
|
||||||
payload_item["thumbnail_url"] = dialog_item.thumbnail_url;
|
payload_item["thumbnail_url"] = dialog_item.thumbnail_url;
|
||||||
payload_item["installed"] = dialog_item.has_local_package;
|
payload_item["installed"] = dialog_item.has_local_package;
|
||||||
|
payload_item["orphaned"] = dialog_item.orphaned;
|
||||||
payload_item["installed_version"] = dialog_item.installed_version;
|
payload_item["installed_version"] = dialog_item.installed_version;
|
||||||
payload_item["latest_version"] = dialog_item.latest_version;
|
payload_item["latest_version"] = dialog_item.latest_version;
|
||||||
return payload_item;
|
return payload_item;
|
||||||
@@ -265,7 +267,8 @@ PluginSource derive_plugin_source(const PluginDescriptor& descriptor)
|
|||||||
const bool is_cloud = descriptor.is_cloud_plugin();
|
const bool is_cloud = descriptor.is_cloud_plugin();
|
||||||
const bool is_mine = is_cloud && has_cloud_meta && descriptor.cloud->is_mine;
|
const bool is_mine = is_cloud && has_cloud_meta && descriptor.cloud->is_mine;
|
||||||
|
|
||||||
// Source is ownership/locality only; issue states never replace this badge.
|
if (is_cloud && has_cloud_meta && descriptor.cloud->orphaned)
|
||||||
|
return PluginSource::Orphaned;
|
||||||
if (is_mine)
|
if (is_mine)
|
||||||
return PluginSource::Mine;
|
return PluginSource::Mine;
|
||||||
if (is_cloud)
|
if (is_cloud)
|
||||||
@@ -278,28 +281,32 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
|
|||||||
PluginAvailableActions available_actions;
|
PluginAvailableActions available_actions;
|
||||||
const bool is_loading = item.status == PluginStatus::Loading;
|
const bool is_loading = item.status == PluginStatus::Loading;
|
||||||
const bool is_cloud = item.is_cloud_plugin;
|
const bool is_cloud = item.is_cloud_plugin;
|
||||||
|
const bool is_orphaned = item.orphaned;
|
||||||
const bool is_mine = item.source == PluginSource::Mine;
|
const bool is_mine = item.source == PluginSource::Mine;
|
||||||
const bool has_local = item.has_local_package;
|
const bool has_local = item.has_local_package;
|
||||||
const bool authorized_for_install = !item.unauthorized;
|
const bool authorized_for_install = !item.unauthorized;
|
||||||
|
|
||||||
available_actions.toggle_installs_cloud_plugin = is_cloud && !has_local && authorized_for_install;
|
available_actions.toggle_installs_cloud_plugin = is_cloud && !is_orphaned && !has_local && authorized_for_install;
|
||||||
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
|
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
|
||||||
|
|
||||||
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
|
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
|
||||||
available_actions.context_actions.push_back(PluginContextAction{id, label, enabled, danger});
|
available_actions.context_actions.push_back(PluginContextAction{id, label, enabled, danger});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (is_cloud) {
|
if (is_cloud && !is_orphaned) {
|
||||||
if (is_mine)
|
if (is_mine)
|
||||||
add_action("delete_mine_plugin", "Delete", true, true);
|
add_action("delete_mine_plugin", "Delete", true, true);
|
||||||
else
|
else
|
||||||
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
|
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
|
||||||
|
} else if (is_orphaned && has_local) {
|
||||||
|
add_action("delete_plugin", "Delete", true, true);
|
||||||
} else if (has_local) {
|
} else if (has_local) {
|
||||||
add_action("delete_plugin", "Delete", true, true);
|
add_action("delete_plugin", "Delete", true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_action("open_folder", "Show in folder", has_local);
|
add_action("open_folder", "Show in folder", has_local);
|
||||||
|
|
||||||
|
if (!is_orphaned)
|
||||||
add_action("reinstall_plugin", "Reinstall");
|
add_action("reinstall_plugin", "Reinstall");
|
||||||
|
|
||||||
return available_actions;
|
return available_actions;
|
||||||
@@ -353,6 +360,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
|||||||
item.error_text = descriptor.normalized_error();
|
item.error_text = descriptor.normalized_error();
|
||||||
item.has_error = descriptor.has_error();
|
item.has_error = descriptor.has_error();
|
||||||
item.is_cloud_plugin = descriptor.is_cloud_plugin();
|
item.is_cloud_plugin = descriptor.is_cloud_plugin();
|
||||||
|
item.orphaned = descriptor.cloud.has_value() && descriptor.cloud->orphaned;
|
||||||
item.has_local_package = descriptor.has_local_package();
|
item.has_local_package = descriptor.has_local_package();
|
||||||
item.unauthorized = descriptor.is_unauthorized();
|
item.unauthorized = descriptor.is_unauthorized();
|
||||||
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
|
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct CloudPluginState
|
|||||||
bool update_available = false; // Cloud version > the local package version.
|
bool update_available = false; // Cloud version > the local package version.
|
||||||
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
|
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
|
||||||
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
|
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
|
||||||
|
bool orphaned = false; // Cloud identity remains locally, but the plugin is no longer subscribed/available.
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class PluginUpdateStatus
|
enum class PluginUpdateStatus
|
||||||
@@ -102,6 +103,8 @@ struct PluginDescriptor
|
|||||||
{
|
{
|
||||||
if (!cloud.has_value())
|
if (!cloud.has_value())
|
||||||
return PluginUpdateStatus::Normal;
|
return PluginUpdateStatus::Normal;
|
||||||
|
if (cloud->orphaned)
|
||||||
|
return PluginUpdateStatus::Normal;
|
||||||
if (cloud->unauthorized)
|
if (cloud->unauthorized)
|
||||||
return PluginUpdateStatus::Unauthorized;
|
return PluginUpdateStatus::Unauthorized;
|
||||||
if (cloud->update_available)
|
if (cloud->update_available)
|
||||||
|
|||||||
@@ -1420,7 +1420,8 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
|
|||||||
|
|
||||||
std::vector<PluginDescriptor> cloud_list{};
|
std::vector<PluginDescriptor> cloud_list{};
|
||||||
std::vector<std::string> not_found{}, unauthorized{};
|
std::vector<std::string> not_found{}, unauthorized{};
|
||||||
if (!m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized)) {
|
const bool cloud_fetch_succeeded = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
|
||||||
|
if (!cloud_fetch_succeeded) {
|
||||||
if (wxTheApp != nullptr) {
|
if (wxTheApp != nullptr) {
|
||||||
GUI::wxGetApp().CallAfter([] {
|
GUI::wxGetApp().CallAfter([] {
|
||||||
if (GUI::wxGetApp().is_closing())
|
if (GUI::wxGetApp().is_closing())
|
||||||
@@ -1435,9 +1436,10 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cloud_fetch_succeeded)
|
||||||
update_cloud_metadata(cloud_list);
|
update_cloud_metadata(cloud_list);
|
||||||
|
|
||||||
{
|
if (cloud_fetch_succeeded) {
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
// Clear the previous cloud verdicts before re-applying the fresh ones.
|
// Clear the previous cloud verdicts before re-applying the fresh ones.
|
||||||
@@ -1446,19 +1448,28 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
|
|||||||
if (!entry.is_cloud_plugin())
|
if (!entry.is_cloud_plugin())
|
||||||
continue;
|
continue;
|
||||||
entry.set_unauthorized(false);
|
entry.set_unauthorized(false);
|
||||||
|
if (entry.cloud.has_value())
|
||||||
|
entry.cloud->orphaned = false;
|
||||||
if (entry.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
|
if (entry.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
|
||||||
entry.clear_error();
|
entry.clear_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const std::string& uuid : not_found) {
|
// A successful subscriptions response may report missing UUIDs explicitly, or it may
|
||||||
|
// simply omit an unsubscribed plugin from `data`. Both cases leave a locally retained
|
||||||
|
// cloud package orphaned. Owned plugins are returned by the separate mine endpoint and
|
||||||
|
// must not be orphaned merely because they are not subscribed.
|
||||||
for (Plugin& plugin : m_plugins) {
|
for (Plugin& plugin : m_plugins) {
|
||||||
PluginDescriptor& entry = plugin.descriptor;
|
PluginDescriptor& entry = plugin.descriptor;
|
||||||
if (!entry.is_cloud_plugin() || entry.cloud_uuid() != uuid)
|
if (!entry.is_cloud_plugin() || entry.cloud->is_mine)
|
||||||
continue;
|
continue;
|
||||||
if (!entry.has_local_package())
|
|
||||||
entry.set_error(CLOUD_PLUGIN_NOT_FOUND_ERROR);
|
const bool explicitly_not_found = std::find(not_found.begin(), not_found.end(), entry.cloud_uuid()) != not_found.end();
|
||||||
break;
|
const bool returned_by_cloud = std::any_of(cloud_list.begin(), cloud_list.end(), [&entry](const PluginDescriptor& cloud_entry) {
|
||||||
}
|
return cloud_entry.cloud_uuid() == entry.cloud_uuid();
|
||||||
|
});
|
||||||
|
entry.cloud->orphaned = explicitly_not_found || !returned_by_cloud;
|
||||||
|
if (entry.cloud->orphaned)
|
||||||
|
entry.cloud->update_available = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const std::string& uuid : unauthorized) {
|
for (const std::string& uuid : unauthorized) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -48,6 +49,18 @@ const char* const CLOUD_PLUGIN_SOURCE = R"PY(# /// script
|
|||||||
# version = "1.0"
|
# version = "1.0"
|
||||||
# ///
|
# ///
|
||||||
print('ok')
|
print('ok')
|
||||||
|
|
||||||
|
import orca
|
||||||
|
class stubscript(orca.script.ScriptPluginCapabilityBase):
|
||||||
|
def get_name(self):
|
||||||
|
return "stubscript"
|
||||||
|
def execute(self):
|
||||||
|
return orca.ExecutionResult.success("Stub orca script.")
|
||||||
|
|
||||||
|
@orca.plugin
|
||||||
|
class stubpackage(orca.base):
|
||||||
|
def register_capabilities(self):
|
||||||
|
orca.register_capability(stubscript)
|
||||||
)PY";
|
)PY";
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -129,4 +142,31 @@ TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginC
|
|||||||
reloaded.load();
|
reloaded.load();
|
||||||
REQUIRE(reloaded.has_config(id));
|
REQUIRE(reloaded.has_config(id));
|
||||||
CHECK(reloaded.get_config(id)->config == configured);
|
CHECK(reloaded.get_config(id)->config == configured);
|
||||||
|
|
||||||
|
// A local package can remain after the cloud subscription disappears. The cloud identity is
|
||||||
|
// retained for diagnosis, but the orphaned state must suppress update availability until the
|
||||||
|
// plugin is returned by a later cloud refresh.
|
||||||
|
PluginDescriptor orphaned_record = cloud_record;
|
||||||
|
orphaned_record.cloud->orphaned = true;
|
||||||
|
orphaned_record.cloud->update_available = true;
|
||||||
|
manager.update_cloud_metadata({orphaned_record});
|
||||||
|
|
||||||
|
const PluginDescriptor orphaned = find_by_uuid();
|
||||||
|
REQUIRE(orphaned.cloud.has_value());
|
||||||
|
CHECK(orphaned.cloud->orphaned);
|
||||||
|
CHECK_FALSE(orphaned.has_error());
|
||||||
|
CHECK(orphaned.get_update_status() == PluginUpdateStatus::Normal);
|
||||||
|
|
||||||
|
// Orphaned is informational only: the local package must remain loadable and usable.
|
||||||
|
std::string load_error;
|
||||||
|
manager.load_plugin(uuid, /*skip_deps=*/true);
|
||||||
|
REQUIRE(manager.wait_for_plugin_load(uuid, std::chrono::seconds(120), load_error));
|
||||||
|
INFO("load error: " << load_error);
|
||||||
|
CHECK(load_error.empty());
|
||||||
|
CHECK(manager.is_plugin_loaded(uuid));
|
||||||
|
CHECK(manager.unload_plugin(uuid));
|
||||||
|
|
||||||
|
// Seeing the plugin in a subsequent cloud response clears the orphaned marker.
|
||||||
|
manager.update_cloud_metadata({cloud_record});
|
||||||
|
CHECK_FALSE(find_by_uuid().cloud->orphaned);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user