diff --git a/resources/web/guide/21/common.css b/resources/web/guide/21/common.css
index e7cd57a32f..992c55d7a1 100644
--- a/resources/web/guide/21/common.css
+++ b/resources/web/guide/21/common.css
@@ -415,7 +415,9 @@ img.ModelThumbnail {
border-width: 1px;
border-style: solid;
border-radius: 4px;
- background-color: inherit;
+ border-color: var(--border-color);
+ background-color: var(--bg-color);
+ color: var(--fg-color-text);
position: absolute;
left: 50%;
top: 200px;
@@ -423,13 +425,17 @@ img.ModelThumbnail {
}
#NoticeBar {
- background-color:#00f0d8;
+ background-color: var(--main-color);
height: 40px;
line-height: 40px;
color: #fff;
text-align: center;
}
+#NoticeBar.notice-error {
+ background-color: var(--button-bg-alert);
+}
+
#NoticeContent {
padding: 4mm 10mm;
}
diff --git a/resources/web/guide/21/common.js b/resources/web/guide/21/common.js
index f7e8b1d409..9a3c76c248 100644
--- a/resources/web/guide/21/common.js
+++ b/resources/web/guide/21/common.js
@@ -37,6 +37,34 @@ function HandleStudio( pVal )
{
HandleModelList(pVal['response']);
}
+ else if(strCmd=='check_new_printers_result')
+ {
+ let button = document.getElementById("CheckNewPrintersBtn");
+ if (button) {
+ button.style.pointerEvents = "auto";
+ button.style.opacity = "1";
+ }
+
+ let noticeBar = document.getElementById("NoticeBar");
+ let noticeText = document.getElementById("NoticeText");
+ let hasError = pVal.hasOwnProperty("error");
+ noticeBar.classList.toggle("notice-error", hasError);
+ if (hasError) {
+ noticeBar.textContent = "Error";
+ noticeText.textContent = pVal["error"];
+ } else if (pVal["vendors"] && pVal["vendors"].length > 0) {
+ noticeBar.textContent = "New printers found";
+ noticeText.textContent = "New printer vendors installed: " + pVal["vendors"].join(", ");
+ } else if (pVal["declined"]) {
+ noticeBar.textContent = "Information";
+ noticeText.textContent = "New printer vendors were found, but installation was cancelled.";
+ } else {
+ noticeBar.textContent = "Information";
+ noticeText.textContent = "No new printers found.";
+ }
+
+ ShowNotice(1);
+ }
}
function HandleModelList( pVal )
@@ -78,9 +106,13 @@ function HandleModelList( pVal )
}
//Update Nozzel Html Append
+ // ORCA: HandleModelList can now be called more than once per dialog (e.g. after a new vendor
+ // is installed via "check for new printers"). pModel always holds the full, current list, so
+ // clear each vendor's printer area before repopulating instead of appending on top of a
+ // previous render.
for( let key in ModelHtml )
{
- $(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").append( ModelHtml[key] );
+ $(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").empty().append( ModelHtml[key] );
}
//Update Checkbox
@@ -343,6 +375,9 @@ function OnExit()
let nTotal=ModelSelect.length;
if( nTotal==0 ) {
+ let noticeBar = document.getElementById("NoticeBar");
+ noticeBar.classList.add("notice-error");
+ noticeBar.textContent = "Error";
ShowNotice(1);
return 0;
}
diff --git a/resources/web/guide/24/24.js b/resources/web/guide/24/24.js
index 74a03db7b9..19ccb892c6 100644
--- a/resources/web/guide/24/24.js
+++ b/resources/web/guide/24/24.js
@@ -36,6 +36,22 @@ function ConfirmSelect()
}
}
+function CheckForNewPrinters()
+{
+ var tSend={};
+ tSend['sequence_id']=Math.round(new Date() / 1000);
+ tSend['command']="check_for_new_printers";
+ tSend['data']={};
+
+ var button = document.getElementById("CheckNewPrintersBtn");
+ if (button) {
+ button.style.pointerEvents = "none";
+ button.style.opacity = "0.6";
+ }
+
+ SendWXMessage( JSON.stringify(tSend) );
+}
+
function CreateNewPrinter()
{
var tSend={};
diff --git a/resources/web/guide/24/index.html b/resources/web/guide/24/index.html
index efb6148e0d..5f0a2407cc 100644
--- a/resources/web/guide/24/index.html
+++ b/resources/web/guide/24/index.html
@@ -82,7 +82,8 @@
Create
-
Confirm
+
Check for new printers
+
Confirm
Cancel
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index 2d0c6a5d8d..020fc0662d 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -42,6 +42,9 @@ namespace Slic3r {
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
+
+constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
+
static const std::string MODELS_STR = "models";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
@@ -1814,7 +1817,10 @@ std::string AppConfig::version_check_url() const
std::string AppConfig::profile_update_url() const
{
- return PROFILE_UPDATE_URL;
+ std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
+ if (orca_updater_url.empty())
+ return PROFILE_UPDATE_URL;
+ return orca_updater_url;
}
bool AppConfig::exists()
diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp
index 6b58fffead..0820f7181b 100644
--- a/src/slic3r/GUI/WebGuideDialog.cpp
+++ b/src/slic3r/GUI/WebGuideDialog.cpp
@@ -23,6 +23,7 @@
#include
#include
+#include
#include
#include
#include
@@ -562,6 +563,78 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
m_ProfileJson["filament"][fName]["selected"] = 1;
}
}
+ else if (strCmd == "check_for_new_printers") {
+ json response = json::object();
+ response["command"] = "check_new_printers_result";
+ // Guide pages currently send sequence_id as a number, while older
+ // pages may send it as a string. Preserve the value without
+ // forcing either representation.
+ if (j.contains("sequence_id"))
+ response["sequence_id"] = j["sequence_id"];
+ else
+ response["sequence_id"] = "";
+
+ if (!m_MainPtr->preset_updater) {
+ response["error"] = "Printer update service is unavailable.";
+ wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
+ wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
+ } else {
+ // Orca: enumerate vendors directly from disk rather than from m_ProfileJson["model"]
+ // — a vendor with no machine models (e.g. a filament-only bundle, or a test fixture
+ // like "test123" with an empty machine_model_list) never gets a "vendor" entry
+ // pushed into "model" by LoadProfileFamily(), so it would be invisible to the
+ // request body and get endlessly re-offered by the server. Scan both the system dir
+ // (already-installed vendors) and the bundled resources dir (shipped-but-not-yet-
+ // installed vendors), same as LoadProfileData() does when building loaded_vendors.
+ std::set system_vendors;
+ for (const auto& dir : {vendor_dir, rsrc_vendor_dir}) {
+ if (!boost::filesystem::exists(dir))
+ continue;
+ for (const auto& entry : boost::filesystem::directory_iterator(dir)) {
+ if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json"))
+ system_vendors.insert(entry.path().stem().string());
+ }
+ }
+ // Orca: check_new_vendors() is async (network + confirmation dialog + download
+ // all happen off the calling thread apart from the dialog itself); guard against
+ // this dialog being closed before the callback fires.
+ wxWeakRef weak_this(this);
+ try {
+ m_MainPtr->preset_updater->check_new_vendors(
+ system_vendors, [weak_this, response](std::vector installed_vendors, bool declined) mutable {
+ if (!weak_this)
+ return;
+
+ // Orca: append the newly installed vendor(s) into the in-memory
+ // profile data (instead of a full LoadProfileData() rescan of every
+ // vendor) and push the refreshed list to the webview, the same way
+ // request_userguide_profile does, so the printer list picks them up
+ // without needing to reopen the guide.
+ for (const auto& vendor_id : installed_vendors) {
+ weak_this->LoadProfileFamily(vendor_id, (weak_this->vendor_dir / (vendor_id + ".json")).string());
+ }
+ if (!installed_vendors.empty()) {
+ json profile_response = json::object();
+ profile_response["command"] = "response_userguide_profile";
+ profile_response["sequence_id"] = "10001";
+ profile_response["response"] = weak_this->m_ProfileJson;
+ wxString profileJS = wxString::Format("HandleStudio(%s)", profile_response.dump(-1, ' ', true));
+ weak_this->RunScript(profileJS);
+ }
+
+ response["vendors"] = installed_vendors;
+ response["declined"] = declined;
+ wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
+ weak_this->RunScript(strJS);
+ });
+ } catch (const std::exception &e) {
+ BOOST_LOG_TRIVIAL(warning) << "Failed to check for new printers: " << e.what();
+ response["error"] = "Failed to check for new printers.";
+ wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
+ wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
+ }
+ }
+ }
else if (strCmd == "user_guide_finish") {
SaveProfile();
@@ -1644,13 +1717,15 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
OneModel["materials"] = pm["default_materials"];
// wxString strCoverPath = wxString::Format("%s\\%s\\%s_cover.png", strFolder, strVendor, std::string(s1.mb_str()));
- std::string cover_file = s1 + "_cover.png";
- boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file).make_preferred();
+ std::string cover_file = s1 + "_cover.png";
+ boost::filesystem::path cover_path = boost::filesystem::absolute(vendor_dir / cover_file).make_preferred();
+ BOOST_LOG_TRIVIAL(info) << "[WebGuideDialog] " << cover_path;
if (!boost::filesystem::exists(cover_path)) {
- cover_path =
- (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") /
- cover_file)
- .make_preferred();
+ cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file)
+ .make_preferred();
+ if (!boost::filesystem::exists(cover_path))
+ cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
+ .make_preferred();
}
OneModel["cover"] = cover_path.string();
diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp
index 2de8716f47..42bd346222 100644
--- a/src/slic3r/Utils/PresetUpdater.cpp
+++ b/src/slic3r/Utils/PresetUpdater.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -129,10 +130,9 @@ struct Update
//BBS: add directory support
void install() const
{
- if (is_directory) {
+ if (is_directory) {
copy_directory_recursively(source, target, file_filter);
- }
- else {
+ } else {
copy_file_fix(source, target);
// A vendor must be installed in exactly one form. Remove the
@@ -147,7 +147,7 @@ struct Update
fs::remove(target.parent_path() / (vendor + ".opc"), ec);
}
}
- }
+ }
friend std::ostream& operator<<(std::ostream& os, const Update &self)
{
@@ -195,6 +195,8 @@ struct Updates
std::vector updates;
};
+static bool reload_configs_update_gui();
+
wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);
wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent);
@@ -222,6 +224,10 @@ struct PresetUpdater::priv
// Per-vendor update checking
std::set checked_vendors;
+ // Orca (PR #130): changelog text for each vendor, captured in memory during
+ // sync_vendor_config()/check_new_vendors() instead of written beside the cache.
+ std::unordered_map vendor_changelogs;
+ mutable std::mutex vendor_changelogs_mutex;
std::vector vendor_check_threads;
std::atomic vendor_check_cancel{false};
@@ -246,6 +252,8 @@ struct PresetUpdater::priv
void parse_version_string(const std::string& body) const;
void sync_resources(std::string http_url, std::map &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
void sync_vendor_config(const std::string& vendor_id);
+ void check_new_vendors(const std::set& system_vendors,
+ std::function, bool)> callback);
void sync_tooltip(std::string http_url, std::string language);
void sync_plugins(std::string http_url, std::string plugin_version);
void sync_printer_config(std::string http_url);
@@ -686,6 +694,9 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION
std::string download_url_str;
+ std::string changelog;
+
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url;
Http::get(url)
.timeout_connect(5)
@@ -698,9 +709,12 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (http_status != 200) return;
try {
json j = json::parse(body);
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body;
+
if (j.contains("vendor_version") && j.contains("download_url")) {
online_version_str = j["vendor_version"].get();
download_url_str = j["download_url"].get();
+ changelog = j.value("changelog", std::string());
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what();
@@ -722,8 +736,11 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
boost::system::error_code ec;
fs::remove_all(cache_profile_path / vendor_id, ec);
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
- fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec);
+ // Orca: the OPC cache is the vendor's whole installation in one file; clear it too.
fs::remove(cache_profile_path / (vendor_id + ".opc"), ec);
+ // Best-effort cleanup of the legacy on-disk changelog written by older builds
+ // (changelogs are now kept in memory - see vendor_changelogs).
+ fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec);
// Download the zip
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading update for " << vendor_id
@@ -761,6 +778,23 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (cancel || vendor_check_cancel) return;
+ const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
+ const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
+ if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) ||
+ fs::is_empty(cached_vendor_folder)) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id
+ << ": expected " << vendor_id << ".json and a non-empty "
+ << vendor_id << " directory";
+ fs::remove_all(cached_vendor_folder, ec);
+ fs::remove(cached_vendor_json, ec);
+ return;
+ }
+
+ {
+ std::lock_guard lock(vendor_changelogs_mutex);
+ vendor_changelogs[vendor_id] = std::move(changelog);
+ }
+
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI";
GUI::wxGetApp().CallAfter([] {
GUI::wxGetApp().check_config_updates_from_updater();
@@ -1167,6 +1201,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
if (!fs::exists(cache_profile_path))
return updates;
+ // Orca (PR #130): vendor changelogs are captured in memory during
+ // sync_vendor_config()/check_new_vendors(), not written beside the cache.
+ std::unordered_map changelogs;
+ {
+ std::lock_guard lock(vendor_changelogs_mutex);
+ changelogs = vendor_changelogs;
+ }
+
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
@@ -1179,6 +1221,21 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
+ // Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty
+ // / preset directory; a stray or half-downloaded .json is
+ // skipped. An .opc cache is a single self-contained file (validated below),
+ // so this check does not apply to it.
+ if (!is_opc_file) {
+ const auto vendor_folder_in_cache = cache_profile_path / vendor_name;
+ if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) ||
+ fs::is_empty(vendor_folder_in_cache)) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for "
+ << vendor_name << ": expected " << vendor_name
+ << ".json and a non-empty " << vendor_name << " directory";
+ continue;
+ }
+ }
+
if (is_vendor_installed(vendor_name)
|| is_opc_file
|| fs::exists(print_in_cache)
@@ -1212,15 +1269,9 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
cache_ver = *config_version;
}
- std::string changelog;
- std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string();
- boost::nowide::ifstream ifs(changelog_file);
- if (ifs) {
- std::ostringstream oss;
- oss<< ifs.rdbuf();
- changelog = oss.str();
- ifs.close();
- }
+ // Orca (PR #130): changelog for this vendor was captured in memory at sync time.
+ const auto changelog_it = changelogs.find(vendor_name);
+ std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string();
if (vendor_ver < cache_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
@@ -1362,6 +1413,9 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
// after the startup printer preset has been restored.
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
+ // Orca (PR #130): the filament library is always installed, so refresh it
+ // from the updater on every startup sync rather than deferring to check_vendor_update().
+ this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY);
//if (p->cancel)
// return;
//remove the tooltip currently
@@ -1394,6 +1448,302 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id)
});
}
+// Orca: ask the server which vendors from `system_vendors` have a profile bundle available that
+// isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently
+// installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps
+// vendor id -> {version, download_url, changelog} for each vendor the server has an update for.
+// Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config()
+// does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates()
+// flow then installs the cached profiles into data_dir()/system.
+//
+// Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract
+// work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only
+// the confirmation dialog (which must run on the UI thread) and the final callback are marshaled
+// back via CallAfter().
+void PresetUpdater::priv::check_new_vendors(const std::set& system_vendors,
+ std::function, bool)> callback)
+{
+ vendor_check_threads.emplace_back([this, system_vendors, callback]() {
+ AppConfig* app_config = GUI::wxGetApp().app_config;
+ std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION);
+
+ auto check_cancel = [this](Http::Progress, bool& cancel_http) {
+ if (cancel || vendor_check_cancel)
+ cancel_http = true;
+ };
+
+ json request_body = json::object();
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:";
+ for (const auto& vendor_id : system_vendors) {
+ // Orca: installed_vendor_version() reads whichever form the vendor is
+ // installed as - the .json profile or the .opc preset cache stamp -
+ // so a cache-only vendor is not reported as version 0.0.0 and then
+ // endlessly re-offered by the server.
+ Semver installed_ver = installed_vendor_version(vendor_id);
+ request_body[vendor_id] = installed_ver.to_string();
+ BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")";
+ }
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url;
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2);
+
+ json response_json;
+ bool got_response = false;
+
+ auto post = Http::post(url);
+
+ post.timeout_connect(5);
+ post.on_progress(check_cancel);
+ post.header("Content-Type", "application/json");
+ post.on_error([](std::string body, std::string error, unsigned http_status) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error;
+ })
+ .on_complete([&response_json, &got_response](std::string body, unsigned http_status) {
+ if (http_status != 200)
+ return;
+ try {
+ json j = json::parse(body);
+ if (j.is_object()) {
+ response_json = std::move(j);
+ got_response = true;
+ }
+ } catch (const std::exception& e) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what();
+ }
+ });
+
+ post.set_post_body(request_body.dump());
+ post.perform_sync();
+
+ if (cancel || vendor_check_cancel)
+ return;
+
+ if (!got_response) {
+ GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
+ return;
+ }
+
+ // Collect candidates before touching the filesystem or network again.
+ struct NewVendorCandidate
+ {
+ std::string vendor_id;
+ Semver version;
+ std::string download_url;
+ std::string changelog;
+ };
+ std::vector candidates;
+ for (auto it = response_json.begin(); it != response_json.end(); ++it) {
+ const json& entry = it.value();
+ if (!entry.is_object())
+ continue;
+ std::string download_url_str = entry.value("download_url", std::string());
+ if (download_url_str.empty())
+ continue;
+
+ NewVendorCandidate candidate;
+ candidate.vendor_id = it.key();
+ auto parsed_ver = Semver::parse(entry.value("version", std::string()));
+ candidate.version = parsed_ver ? *parsed_ver : Semver();
+ candidate.download_url = std::move(download_url_str);
+ candidate.changelog = entry.value("changelog", std::string());
+ candidates.push_back(std::move(candidate));
+ }
+
+ if (candidates.empty()) {
+ GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
+ return;
+ }
+
+ // Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual
+ // download/install work is dispatched back onto a new background thread from there,
+ // same as check_vendor_update() does for a single vendor.
+ GUI::wxGetApp().CallAfter([this, candidates, callback]() {
+ std::vector updates_msg;
+ for (const auto& candidate : candidates)
+ updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog);
+
+ GUI::MsgUpdateConfig dlg(updates_msg);
+ if (dlg.ShowModal() != wxID_OK) {
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors";
+ callback({}, true);
+ return;
+ }
+
+ // Orca: the actual download runs on a background thread below (so it doesn't block the
+ // UI), but that also means nothing visibly happens for the several seconds it can take
+ // (longer still if a retry kicks in) — push a notification so it's clear work is
+ // ongoing rather than looking hung.
+ {
+ std::string vendor_list;
+ for (const auto& candidate : candidates) {
+ if (!vendor_list.empty())
+ vendor_list += ", ";
+ vendor_list += candidate.vendor_id;
+ }
+ GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
+ _u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("..."));
+ }
+
+ vendor_check_threads.emplace_back([this, candidates, callback]() {
+ auto check_cancel = [this](Http::Progress, bool& cancel_http) {
+ if (cancel || vendor_check_cancel)
+ cancel_http = true;
+ };
+
+ std::vector new_vendor_ids;
+ std::vector failed_vendor_ids;
+ auto cache_profile_path = cache_path / "profiles";
+ fs::create_directories(cache_profile_path);
+ boost::system::error_code ec;
+
+ for (const auto& candidate : candidates) {
+ if (cancel || vendor_check_cancel)
+ break;
+
+ const std::string& vendor_id = candidate.vendor_id;
+ const std::string& download_url_str = candidate.download_url;
+ std::string changelog = candidate.changelog;
+
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string();
+
+ // Clear only this vendor's cached data, same as sync_vendor_config().
+ fs::remove_all(cache_profile_path / vendor_id, ec);
+ fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
+
+ fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION);
+ bool download_ok = false;
+
+ // Orca: same retry pattern as Plater.cpp's project download — a single-shot
+ // 5s connect timeout against GitHub's redirect chain is prone to transient
+ // failures (DNS/connect hiccups) that succeed a moment later, so retry a few
+ // times before giving up rather than failing the whole vendor on one blip.
+ int retry_count = 0;
+ const int max_retries = 3;
+ bool keep_trying = true;
+ while (keep_trying && retry_count < max_retries) {
+ retry_count++;
+ Http::get(download_url_str)
+ .timeout_connect(5)
+ .on_progress(check_cancel)
+ .on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id
+ << " (attempt " << retry_count << "/" << max_retries << "): " << error;
+ })
+ .on_complete([&](std::string body, unsigned http_status) {
+ if (http_status != 200)
+ return;
+ fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc);
+ if (!file.good())
+ return;
+ file.write(body.c_str(), body.size());
+ file.close();
+ if (file.good())
+ download_ok = true;
+ })
+ .perform_sync();
+
+ keep_trying = !download_ok && !(cancel || vendor_check_cancel);
+ }
+
+ if (!download_ok || cancel || vendor_check_cancel) {
+ if (!download_ok)
+ failed_vendor_ids.push_back(vendor_id);
+ continue;
+ }
+
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id;
+ if (!extract_file(download_file, cache_profile_path)) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id;
+ fs::remove(download_file, ec);
+ failed_vendor_ids.push_back(vendor_id);
+ continue;
+ }
+ fs::remove(download_file, ec);
+
+ const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
+ const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
+ if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) ||
+ fs::is_empty(cached_vendor_folder)) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id
+ << ".json and a non-empty " << vendor_id << " directory";
+ fs::remove_all(cached_vendor_folder, ec);
+ fs::remove(cached_vendor_json, ec);
+ failed_vendor_ids.push_back(vendor_id);
+ continue;
+ }
+
+ {
+ std::lock_guard lock(vendor_changelogs_mutex);
+ vendor_changelogs[vendor_id] = std::move(changelog);
+ }
+
+ new_vendor_ids.push_back(vendor_id);
+ }
+
+ if (!new_vendor_ids.empty()) {
+ // Orca: the user already confirmed via the dialog above, so install right away
+ // instead of routing through check_config_updates_from_updater(), which only
+ // queues a passive notification (meant for the silent background per-vendor
+ // check) requiring yet another click + confirmation before anything is copied
+ // into data_dir()/system.
+ GUI::wxGetApp().CallAfter([this, new_vendor_ids] {
+ AppConfig* app_config = GUI::wxGetApp().app_config;
+ Updates updates = get_config_updates(app_config->orig_version());
+
+ // Only install the vendors just confirmed; leave any other unrelated
+ // pending cached update (from a background sync_vendor_config()) alone,
+ // still gated behind its own notification/confirmation.
+ std::set confirmed(new_vendor_ids.begin(), new_vendor_ids.end());
+ Updates filtered;
+ for (auto& update : updates.updates)
+ if (confirmed.count(update.vendor))
+ filtered.updates.push_back(std::move(update));
+
+ if (filtered.updates.empty()) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected";
+ return;
+ }
+
+ if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) {
+ BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors";
+ return;
+ }
+
+ BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed";
+ for (const auto& vendor_id : new_vendor_ids) {
+ Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id);
+ GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
+ GUI::NotificationType::PresetUpdateFinished,
+ GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
+ _u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string());
+ }
+ });
+ }
+
+ if (!failed_vendor_ids.empty()) {
+ GUI::wxGetApp().CallAfter([failed_vendor_ids] {
+ std::string vendor_list;
+ for (const auto& vendor_id : failed_vendor_ids) {
+ if (!vendor_list.empty())
+ vendor_list += ", ";
+ vendor_list += vendor_id;
+ }
+ GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
+ _u8L("Failed to download vendor profile(s): ") + vendor_list);
+ });
+ }
+
+ GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); });
+ });
+ });
+ });
+}
+
+void PresetUpdater::check_new_vendors(const std::set& system_vendors,
+ std::function, bool)> callback)
+{
+ p->check_new_vendors(system_vendors, std::move(callback));
+}
+
void PresetUpdater::slic3r_update_notify()
{
if (! p->enabled_version_check)
@@ -1415,10 +1765,9 @@ static bool reload_configs_update_gui()
GUI::wxGetApp().load_current_presets();
GUI::wxGetApp().plater()->set_bed_shape();
- return true;
+ return true;
}
-
PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const
{
if (! p->enabled_config_update) { return R_NOOP; }
diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp
index 27ec6748f2..ccb0902fd5 100644
--- a/src/slic3r/Utils/PresetUpdater.hpp
+++ b/src/slic3r/Utils/PresetUpdater.hpp
@@ -1,7 +1,9 @@
#ifndef slic3r_PresetUpdate_hpp_
#define slic3r_PresetUpdate_hpp_
+#include
#include
+#include
#include
#include
@@ -59,6 +61,12 @@ public:
void on_update_notification_confirm();
void do_printer_config_update();
void check_vendor_update(const std::string& vendor_id);
+ // Orca: async, mirrors check_vendor_update()/sync_vendor_config() — the network query and any
+ // download/install work happen on a background thread; only the confirmation dialog runs on
+ // the UI thread. `callback` is invoked on the UI thread with the ids of vendors that were
+ // installed (empty if none were found, or the user declined) and whether the user declined.
+ void check_new_vendors(const std::set& system_vendors,
+ std::function installed_vendors, bool declined)> callback);
bool version_check_enabled() const;