* Add OrcaCloud sync platform and preset bundle sharing system

  Introduce OrcaCloud, a cloud sync platform for user presets, alongside
  a preset bundle system that enables sharing printer/filament/process
  profiles as local exportable bundles or subscribed cloud bundles.

  OrcaCloud platform:
  - Auth to Orca Cloud
  - Encrypted token storage (file-based or system keychain)
  - User preset sync with
  - Profile migration from default/bambu folders on first login
  - Homepage integration with entrance to cloud.orcaslicer.com

  Preset bundles:
  - Local bundle import/export with bundle_structure.json metadata
  - Subscribed cloud bundles with version-based update checking
  - Thread-safe concurrent bundle access with read-write mutex
  - Canonical bundle preset naming (_local/<id>/... and _subscribed/<id>/...)
  - Bundle presets are read-only; grouped under subheaders in combo boxes
  - PresetBundleDialog with auto-sync toggle, refresh, update notifications
  - Hyperlinked bundle names to cloud bundle pages

  Co-authored-by: Sabriel Koh <sabrielkcr@gmail.com>
  Co-authored-by: Derrick <derrick992110@gmail.com>
  Co-authored-by: Mykola Nahirnyi <mnahirnyi@amcbridge.com>
  Co-authored-by: Ian Chua <iancrb00@gmail.com>
  Co-authored-by: Draginraptor <draginraptor@gmail.com>
  Co-authored-by: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com>
  Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
  Co-authored-by: Ocraftyone <Ocraftyone@users.noreply.github.com>
  Co-authored-by: yw4z <ywsyildiz@gmail.com>
  Co-authored-by: peterm-m <101202951+peterm-m@users.noreply.github.com>

* Fixed an issue on Windows it failed to login Orca Cloud with Google account
This commit is contained in:
SoftFever
2026-05-01 18:01:29 +08:00
committed by GitHub
parent e54e7a61c0
commit c04be9ab37
113 changed files with 8690 additions and 3466 deletions
+86 -37
View File
@@ -17,6 +17,8 @@
#include <wx/wfstream.h>
#include <boost/cast.hpp>
#include <boost/asio.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/lexical_cast.hpp>
#include <nlohmann/json.hpp>
@@ -40,13 +42,78 @@ END_EVENT_TABLE()
int ZUserLogin::web_sequence_id = 20000;
ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_ANY, "OrcaSlicer")
namespace {
int reserve_loopback_port()
{
try {
boost::asio::io_service io_service;
boost::asio::ip::tcp::acceptor acceptor(io_service, {boost::asio::ip::tcp::v4(), 0});
return static_cast<int>(acceptor.local_endpoint().port());
} catch (...) {
return 0;
}
}
std::string rewrite_loopback_url(std::string url, int port)
{
if (port <= 0) {
return url;
}
const std::string old_port = std::to_string(LOCALHOST_PORT);
const std::string new_port = std::to_string(port);
boost::replace_all(url, std::string(LOCALHOST_URL) + old_port, std::string(LOCALHOST_URL) + new_port);
boost::replace_all(url, "http://127.0.0.1:" + old_port, "http://127.0.0.1:" + new_port);
boost::replace_all(url, "http%3A%2F%2Flocalhost%3A" + old_port, "http%3A%2F%2Flocalhost%3A" + new_port);
boost::replace_all(url, "http%3A%2F%2F127.0.0.1%3A" + old_port, "http%3A%2F%2F127.0.0.1%3A" + new_port);
return url;
}
}
int ZUserLogin::ensure_loopback_port()
{
if (m_loopback_port <= 0) {
m_loopback_port = reserve_loopback_port();
}
int port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT;
wxGetApp().start_http_server(port, m_cloud_agent->get_id());
return port;
}
ZUserLogin::ZUserLogin(std::shared_ptr<ICloudServiceAgent> cloud_agent)
: wxDialog((wxWindow*) (wxGetApp().mainframe), wxID_ANY, "OrcaSlicer"), m_cloud_agent(cloud_agent)
{
SetBackgroundColour(*wxWHITE);
const auto bblnetwork_enabled =wxGetApp().app_config->get_bool("installed_networking");
// Url
NetworkAgent* agent = wxGetApp().getAgent();
if (!agent && bblnetwork_enabled) {
if (!m_cloud_agent) {
wxBoxSizer* m_sizer_main = new wxBoxSizer(wxVERTICAL);
auto m_line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
m_sizer_main->Add(m_line_top, 0, wxEXPAND, 0);
auto* m_message = new wxStaticText(this, wxID_ANY,
_L("Cloud agent is not available. Please restart OrcaSlicer and try again."),
wxDefaultPosition, wxDefaultSize, 0);
m_message->SetForegroundColour(*wxBLACK);
m_message->Wrap(FromDIP(360));
m_sizer_main->Add(m_message, 0, wxALIGN_CENTER | wxALL, FromDIP(15));
m_sizer_main->Add(0, 0, 1, wxBOTTOM, 10);
SetSizer(m_sizer_main);
m_sizer_main->SetSizeHints(this);
Layout();
Fit();
CentreOnParent();
wxGetApp().UpdateDlgDarkUI(this);
return;
}
const auto bblnetwork_enabled = wxGetApp().app_config->get_bool("installed_networking");
if (m_cloud_agent->get_id() == BBL_CLOUD_PROVIDER && !bblnetwork_enabled) {
SetBackgroundColour(*wxWHITE);
@@ -74,20 +141,16 @@ ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_AN
Layout();
Fit();
CentreOnParent();
}
else {
// Get the login URL from the cloud service agent
} else {
// Get the login URL from the injected cloud service agent
wxString strlang = wxGetApp().current_language_code_safe();
strlang.Replace("_", "-");
TargetUrl = wxString::FromUTF8(agent->get_cloud_login_url(strlang.ToStdString()));
m_networkOk = TargetUrl.StartsWith("file://");
TargetUrl = wxString::FromUTF8(m_cloud_agent->get_cloud_login_url(strlang.ToStdString()));
BOOST_LOG_TRIVIAL(info) << "login url = " << TargetUrl.ToStdString();
m_bbl_user_agent = wxString::Format("BBL-Slicer/v%s", wxGetApp().get_bbl_client_version());
// set the frame icon
// Create the webview
m_browser = WebView::CreateWebView(this, TargetUrl);
if (m_browser == nullptr) {
@@ -97,12 +160,6 @@ ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_AN
m_browser->Hide();
m_browser->SetSize(0, 0);
// Log backend information
// wxLogMessage(wxWebView::GetBackendVersionInfo().ToString());
// wxLogMessage("Backend: %s Version: %s",
// m_browser->GetClassInfo()->GetClassName(),wxWebView::GetBackendVersionInfo().ToString());
// wxLogMessage("User Agent: %s", m_browser->GetUserAgent());
// Connect the webview events
Bind(wxEVT_WEBVIEW_NAVIGATING, &ZUserLogin::OnNavigationRequest, this, m_browser->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATED, &ZUserLogin::OnNavigationComplete, this, m_browser->GetId());
@@ -113,19 +170,15 @@ ZUserLogin::ZUserLogin() : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_AN
Bind(wxEVT_WEBVIEW_FULLSCREEN_CHANGED, &ZUserLogin::OnFullScreenChanged, this, m_browser->GetId());
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ZUserLogin::OnScriptMessage, this, m_browser->GetId());
// Connect the idle events
// Bind(wxEVT_IDLE, &ZUserLogin::OnIdle, this);
// Bind(wxEVT_CLOSE_WINDOW, &ZUserLogin::OnClose, this);
// UI
SetTitle(_L("Login"));
// Set a more sensible size for web browsing
wxSize pSize = FromDIP(wxSize(650, 840));
SetSize(pSize);
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL);
int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL);
int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0;
wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY);
Move(tmpPT);
}
@@ -221,8 +274,7 @@ void ZUserLogin::OnDocumentLoaded(wxWebViewEvent &evt)
{
// Only notify if the document is the main frame, not a subframe
wxString tmpUrl = evt.GetURL();
NetworkAgent* agent = wxGetApp().getAgent();
std::string strHost = agent->get_cloud_service_host();
std::string strHost = m_cloud_agent->get_cloud_service_host();
if (tmpUrl.StartsWith("file://") || tmpUrl.Contains(strHost)) {
m_networkOk = true;
@@ -270,11 +322,10 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt)
json j = json::parse(into_u8(str_input));
wxString strCmd = j["command"];
NetworkAgent* agent = wxGetApp().getAgent();
if (agent && strCmd == "get_login_cmd" && agent->get_cloud_agent()) {
if (m_cloud_agent && strCmd == "get_login_cmd") {
// Return login config (backend_url, apikey, pkce)
// WebView handles provider selection internally
std::string login_cmd = agent->build_login_cmd();
std::string login_cmd = m_cloud_agent->build_login_cmd();
m_loopback_port = 0;
try {
json cfg = json::parse(login_cmd);
@@ -329,13 +380,11 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt)
// Handle message after modal dialog ends to avoid deadlock
// Use wxTheApp->CallAfter to ensure it runs after modal loop exits
wxTheApp->CallAfter([message_json]() {
wxGetApp().handle_script_message(message_json);
});
const auto provider = m_cloud_agent->get_id();
wxTheApp->CallAfter([message_json, provider]() { wxGetApp().handle_script_message(message_json, provider); });
}
else if (strCmd == "get_localhost_url") {
int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT;
wxGetApp().start_http_server(loopback_port);
int loopback_port = ensure_loopback_port();
std::string sequence_id = j["sequence_id"].get<std::string>();
CallAfter([this, sequence_id] {
json ack_j;
@@ -351,8 +400,8 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt)
else if (strCmd == "thirdparty_login") {
if (j["data"].contains("url")) {
std::string jump_url = j["data"]["url"].get<std::string>();
int loopback_port = m_loopback_port > 0 ? m_loopback_port : LOCALHOST_PORT;
wxGetApp().start_http_server(loopback_port);
int loopback_port = ensure_loopback_port();
jump_url = rewrite_loopback_url(jump_url, loopback_port);
CallAfter([this, jump_url] {
wxString url = wxString::FromUTF8(jump_url);
wxLaunchDefaultBrowser(url);