* 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 8691 additions and 3467 deletions

View File

@@ -0,0 +1,75 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="max-age=0" />
<title>Export Presets</title>
<link rel="stylesheet" href="./styles.css" />
<link rel="stylesheet" type="text/css" href="../../include/global.css" /> <!-- ORCA One for all-->
<link rel="stylesheet" type="text/css" href="../css/common.css" />
<link rel="stylesheet" type="text/css" href="../css/dark.css" />
<script type="text/javascript" src="../js/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../js/json2.js"></script>
<script type="text/javascript" src="../../data/text.js"></script>
<script type="text/javascript" src="../js/globalapi.js"></script>
<script type="text/javascript" src="../js/common.js"></script>
<script src="./index.js"></script>
</head>
<body onLoad="OnInit()">
<!-- ORCA column browser -->
<div class="cbr-browser-container">
<div class="cbr-column">
<div class="cbr-column-title-container">
<div class="search-icon"></div>
<input type="text" class="cbr-search-bar" placeholder=" " tabindex="1"/>
<span class="cbr-search-placeholder trans" tid="t15">printer</span>
<div class="clear-icon"></div>
</div>
<div class="cbr-content thin-scroll" id="MachineList">
<div class="CValues">
<label><input type="checkbox" mode="all" onClick="ChooseAllMachine()" /><span class="trans" tid="t11">all</span></label>
</div>
<div class="cbr-no-items">No items</div>
</div>
</div>
<div class="cbr-column">
<div class="cbr-column-title-container">
<div class="search-icon"></div>
<input type="text" class="cbr-search-bar" placeholder=" " tabindex="2"/>
<span class="cbr-search-placeholder trans" tid="t16">filament</span>
<div class="clear-icon"></div>
</div>
<div class="cbr-content thin-scroll" id="FilatypeList">
<div class="CValues">
<label><input type="checkbox" class="trans" tid="t11" onClick="ChooseAllFilament()" /><span class="trans" tid="t11">all</span></label>
</div>
<div class="cbr-no-items">No items</div>
</div>
</div>
<div class="cbr-column">
<div class="cbr-column-title-container">
<div class="search-icon"></div>
<input type="text" class="cbr-search-bar" placeholder=" " tabindex="3"/>
<span class="cbr-search-placeholder trans" tid="t17">presets</span>
<div class="clear-icon"></div>
</div>
<div class="cbr-content thin-scroll" id="PresetList">
<div class="CValues">
<label><input type="checkbox" class="trans" tid="t11" onClick="ChooseAllPreset()" /><span class="trans" tid="t11">all</span></label>
</div>
<div class="cbr-no-items">No items</div>
</div>
</div>
</div>
<div id="AcceptArea">
<div id="export_cloud_btn" class="ButtonStyleConfirm ButtonTypeChoice">Export to OrcaCloud</div>
<div id="export_local_btn" class="ButtonStyleRegular ButtonTypeChoice">Export to folder</div>
<div id="close_btn" class="ButtonStyleRegular ButtonTypeChoice">Close</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,374 @@
var g_profile = {
machines: [],
filaments: [],
presets: []
};
var g_search = {
machine: "",
filament: "",
preset: ""
};
function OnInit()
{
if (typeof TranslatePage === "function")
TranslatePage();
InstallInputSafeKeydown();
BindSearchInputs();
BindClearIcons();
BindBottomButtons();
// Always load demo data first so the page works without C++ backend.
// LoadDemoProfile();
RequestProfile();
}
function InstallInputSafeKeydown()
{
// common.js blocks all key events globally; allow typing in text inputs.
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
var target = e && e.target ? e.target : null;
var tag = target && target.tagName ? String(target.tagName).toUpperCase() : "";
var type = target && target.type ? String(target.type).toLowerCase() : "";
var editable =
!!(target && target.isContentEditable) ||
tag === "TEXTAREA" ||
(tag === "INPUT" && type !== "checkbox" && type !== "radio" && type !== "button" && type !== "submit");
if (editable)
return true;
if (e && e.keyCode === 27 && typeof ClosePage === "function")
ClosePage();
if (window.event) {
try { e.keyCode = 0; } catch (err) { }
e.returnValue = false;
}
if (e && typeof e.preventDefault === "function")
e.preventDefault();
return false;
};
}
function RequestProfile()
{
SendMessage("request_export_preset_profile", {});
}
function HandleStudio(pVal)
{
var payload = (typeof pVal === "string") ? SafeJsonParse(pVal) : pVal;
if (!payload || typeof payload !== "object")
return;
var cmd = String(payload.command || "");
if (cmd === "response_export_preset_profile" ) {
ApplyProfile(payload.data);
}
}
function ApplyProfile(profile)
{
g_profile.machines = BuildNameRows(profile.printers);
g_profile.filaments = BuildNameRows(profile.filaments);
g_profile.presets = BuildNameRows(profile.process);
RenderColumn("MachineList", g_profile.machines, "mode", "MachineClick");
RenderColumn("FilatypeList", g_profile.filaments, "filatype", "FilaClick");
RenderColumn("PresetList", g_profile.presets, "preset", "PresetClick");
ApplyColumnSearch("MachineList", g_search.machine);
ApplyColumnSearch("FilatypeList", g_search.filament);
ApplyColumnSearch("PresetList", g_search.preset);
}
function BuildNameRows(names)
{
var src = Array.isArray(names) ? names : [];
var out = [];
for (var n = 0; n < src.length; n++) {
var row = src[n];
if (row === undefined || row === null)
continue;
var name = String(row);
out.push({ id: name, label: name, checked: false });
}
return out;
}
function RenderColumn(listId, items, attrName, onChangeFn)
{
var root = $("#" + listId + " .CValues");
if (!root.length)
return;
root.find("label:gt(0)").remove();
var html = "";
for (var n = 0; n < items.length; n++) {
var one = items[n];
html += '<label data-dynamic="1">' +
'<input type="checkbox" data-key="' + EscapeAttr(one.id) + '" ' + attrName + '="' + EscapeAttr(one.id) + '"' +
(one.checked ? ' checked="checked"' : "") +
' onChange="' + onChangeFn + '()" />' +
'<span title="' + EscapeAttr(one.label) + '">' + EscapeHtml(one.label) + '</span>' +
'</label>';
}
root.append(html);
SyncMasterCheckbox(listId);
ToggleNoItems(listId, items.length === 0);
}
function ChooseAllMachine()
{
var checked = !!$("#MachineList .CValues input:first").prop("checked");
$("#MachineList .CValues input:gt(0)").prop("checked", checked);
SyncListFromDom("MachineList", g_profile.machines);
}
function MachineClick()
{
SyncMasterCheckbox("MachineList");
SyncListFromDom("MachineList", g_profile.machines);
}
function ChooseAllFilament()
{
var checked = !!$("#FilatypeList .CValues input:first").prop("checked");
$("#FilatypeList .CValues input:gt(0)").prop("checked", checked);
SyncListFromDom("FilatypeList", g_profile.filaments);
}
function FilaClick()
{
SyncMasterCheckbox("FilatypeList");
SyncListFromDom("FilatypeList", g_profile.filaments);
}
function ChooseAllPreset()
{
var checked = !!$("#PresetList .CValues input:first").prop("checked");
$("#PresetList .CValues input:gt(0)").prop("checked", checked);
SyncListFromDom("PresetList", g_profile.presets);
}
function PresetClick()
{
SyncMasterCheckbox("PresetList");
SyncListFromDom("PresetList", g_profile.presets);
}
function SyncMasterCheckbox(listId)
{
var all = $("#" + listId + " .CValues input:gt(0)");
var master = $("#" + listId + " .CValues input:first");
if (!all.length) {
master.prop("checked", false);
return;
}
master.prop("checked", all.length === all.filter(":checked").length);
}
function SyncListFromDom(listId, store)
{
var map = {};
for (var n = 0; n < store.length; n++)
map[store[n].id] = store[n];
$("#" + listId + " .CValues input:gt(0)").each(function () {
var id = String($(this).attr("data-key") || "");
if (map[id])
map[id].checked = !!$(this).prop("checked");
});
}
function BindSearchInputs()
{
var inputs = document.querySelectorAll(".cbr-search-bar");
if (inputs.length > 0) {
inputs[0].addEventListener("input", function () {
g_search.machine = String(this.value || "").toLowerCase();
ApplyColumnSearch("MachineList", g_search.machine);
});
}
if (inputs.length > 1) {
inputs[1].addEventListener("input", function () {
g_search.filament = String(this.value || "").toLowerCase();
ApplyColumnSearch("FilatypeList", g_search.filament);
});
}
if (inputs.length > 2) {
inputs[2].addEventListener("input", function () {
g_search.preset = String(this.value || "").toLowerCase();
ApplyColumnSearch("PresetList", g_search.preset);
});
}
}
function ApplyColumnSearch(listId, query)
{
var rows = $("#" + listId + " .CValues label:gt(0)");
var visibleCount = 0;
rows.each(function () {
var row = $(this);
var text = String(row.text() || "").toLowerCase();
var key = String(row.find("input").attr("data-key") || "").toLowerCase();
if (!query || text.indexOf(query) >= 0 || key.indexOf(query) >= 0) {
row.show();
visibleCount++;
}
else {
row.hide();
}
});
ToggleNoItems(listId, visibleCount === 0);
}
function ToggleNoItems(listId, show)
{
var node = $("#" + listId + " .cbr-no-items");
if (!node.length)
return;
if (show)
node.addClass("show");
else
node.removeClass("show");
}
function BindClearIcons()
{
var icons = document.querySelectorAll(".clear-icon");
for (var n = 0; n < icons.length; n++) {
icons[n].addEventListener("click", function () {
var parent = this.parentElement;
if (!parent)
return;
var input = parent.querySelector("input[type='text']");
if (!input)
return;
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true }));
input.focus();
});
}
}
function BindBottomButtons()
{
var backBtn = document.getElementById("back_btn");
var exportCloud = document.getElementById("export_cloud_btn")
var exportLocal = document.getElementById("export_local_btn");
var closeBtn = document.getElementById("close_btn");
backBtn?.addEventListener("click", function () {
SendMessage("navigate_back", {});
});
exportLocal?.addEventListener("click", function () {
SendMessage("export_local", BuildResultPayload());
});
closeBtn?.addEventListener("click", () => {
const tSend = {
sequence_id: Math.round(Date.now() / 1000),
command: "close_page"
};
SendWXMessage(JSON.stringify(tSend));
});
}
function BuildResultPayload()
{
return {
machines: g_profile.machines.filter(function (x) { return x.checked; }).map(function (x) { return x.id; }),
filaments: g_profile.filaments.filter(function (x) { return x.checked; }).map(function (x) { return x.id; }),
presets: g_profile.presets.filter(function (x) { return x.checked; }).map(function (x) { return x.id; })
};
}
function LoadDemoProfile()
{
ApplyProfile({
machines: [
{ id: "printer_x1c_04", name: "X1 Carbon 0.4 nozzle", selected: 1 },
{ id: "printer_p1s_04", name: "P1S 0.4 nozzle", selected: 1 },
{ id: "printer_a1_04", name: "A1 0.4 nozzle", selected: 0 },
{ id: "printer_prusa_mk4_04", name: "Prusa MK4 0.4 nozzle", selected: 1 }
],
filaments: [
{ id: "filament_generic_pla", name: "Generic PLA", selected: 1 },
{ id: "filament_generic_petg", name: "Generic PETG", selected: 1 },
{ id: "filament_bambu_abs", name: "Bambu ABS", selected: 0 },
{ id: "filament_esun_pla_plus", name: "eSUN PLA+", selected: 1 }
],
presets: [
{ id: "preset_quality_020", name: "Quality 0.20mm", selected: 1 },
{ id: "preset_quality_012", name: "Quality 0.12mm", selected: 0 },
{ id: "preset_speed_024", name: "Speed 0.24mm", selected: 1 },
{ id: "preset_draft_028", name: "Draft 0.28mm", selected: 0 }
]
});
}
function SendMessage(command, data)
{
var msg = {};
msg.sequence_id = Math.round(new Date() / 1000);
msg.command = command;
if (data && typeof data === "object")
msg.data = data;
if (typeof SendWXMessage === "function")
SendWXMessage(JSON.stringify(msg));
}
function SafeJsonParse(str)
{
try {
return JSON.parse(str);
}
catch (err) {
return null;
}
}
function EscapeHtml(str)
{
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function EscapeAttr(str)
{
return EscapeHtml(str);
}

View File

@@ -0,0 +1,191 @@
:root {
--cbr-border-color: #d2d2d7;
--cbr-header-bg: #f6f7f9;
--cbr-panel-bg: #ffffff;
--cbr-input-bg: #ffffff;
--cbr-input-focus-bg: #f2f8f7;
--cbr-label-color: #7b7b84;
--cbr-icon-color: #75757f;
}
@media (prefers-color-scheme: dark) {
:root {
--cbr-border-color: #4a4a51;
--cbr-header-bg: #2f2f34;
--cbr-panel-bg: #2d2d31;
--cbr-input-bg: #2d2d31;
--cbr-input-focus-bg: #3b3b41;
--cbr-label-color: #b9b9bc;
--cbr-icon-color: #b9b9bc;
}
}
.cbr-browser-container {
flex: 1 1 auto;
min-height: 0;
margin: 10px 15px 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: minmax(0, 1fr);
border: 1px solid var(--cbr-border-color);
background: var(--cbr-panel-bg);
box-sizing: border-box;
}
.cbr-column {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.cbr-column:not(:last-child) {
border-right: 1px solid var(--cbr-border-color);
}
.cbr-column-title-container {
position: relative;
display: flex;
align-items: center;
min-height: 36px;
padding: 3px 6px;
background: var(--cbr-header-bg);
border-bottom: 1px solid var(--cbr-border-color);
}
.cbr-search-bar {
width: 100%;
min-width: 0;
box-sizing: border-box;
font-size: 14px;
line-height: 22px;
padding: 2px 26px 2px 26px;
border: 1px solid transparent;
border-radius: 4px;
background: var(--cbr-input-bg);
}
.cbr-search-bar:hover,
.cbr-search-bar:focus {
border-color: var(--main-color);
outline: none;
}
.cbr-search-bar:focus {
background: var(--cbr-input-focus-bg);
}
.cbr-search-placeholder {
position: absolute;
left: 33px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
line-height: 20px;
color: var(--cbr-label-color);
pointer-events: none;
}
.cbr-search-bar:not(:placeholder-shown) + .cbr-search-placeholder {
opacity: 0;
}
.search-icon,
.clear-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--cbr-icon-color);
line-height: 1;
}
.search-icon {
left: 11px;
pointer-events: none;
}
.search-icon::before {
content: "";
width: 9px;
height: 9px;
box-sizing: border-box;
border: 1.8px solid currentColor;
border-radius: 50%;
}
.search-icon::after {
content: "";
position: absolute;
width: 5px;
height: 1.8px;
background: currentColor;
transform: rotate(45deg);
right: 0;
bottom: 2px;
}
.clear-icon {
right: 11px;
cursor: pointer;
display: none;
}
.clear-icon::before {
content: "";
position: absolute;
width: 10px;
height: 1.8px;
background: currentColor;
transform: rotate(45deg);
}
.clear-icon::after {
content: "";
position: absolute;
width: 10px;
height: 1.8px;
background: currentColor;
transform: rotate(-45deg);
}
.cbr-search-bar:not(:placeholder-shown) ~ .clear-icon {
display: flex;
}
.cbr-content {
min-height: 0;
overflow-y: auto;
padding: 4px 8px;
}
.cbr-column .CValues {
display: grid;
}
.CValues label {
margin: 0 !important;
padding: 1px 0;
}
.cbr-content .cbr-no-items {
display: none;
color: var(--cbr-label-color);
font-size: 12px;
padding-top: 6px;
}
.cbr-content .cbr-no-items.show {
display: block;
}
#AcceptArea {
border-top: 1px solid var(--cbr-border-color);
}