mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-05-18 11:02:08 +00:00
Introducing Orca Cloud: https://cloud.orcaslicer.com (#13414)
* 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:
71
resources/web/dialog/PresetBundleDialog/index.html
Normal file
71
resources/web/dialog/PresetBundleDialog/index.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Preset Bundle</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="23.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 type="text/javascript" src="./23.js"></script> -->
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body onLoad="OnInit()">
|
||||
<div class="app">
|
||||
<div class="split">
|
||||
<div class="top-toolbar">
|
||||
<button id="refresh_btn" class="ButtonStyleConfirm ButtonTypeChoice toolbar-btn">Refresh</button>
|
||||
|
||||
<!-- <label class="auto-update-switch" for="auto_update_toggle">
|
||||
<span class="auto-update-label">Auto update</span>
|
||||
<input id="auto_update_toggle" type="checkbox" />
|
||||
<span class="auto-update-slider" aria-hidden="true">
|
||||
<span class="auto-update-text auto-update-text-off">OFF</span>
|
||||
<span class="auto-update-text auto-update-text-on">ON</span>
|
||||
</span>
|
||||
</label> -->
|
||||
</div>
|
||||
|
||||
<section class="pane top">
|
||||
<div class="hdr top-cols">
|
||||
<span>Name</span>
|
||||
<span>Type</span>
|
||||
<span>Version</span>
|
||||
<span>Update</span>
|
||||
</div>
|
||||
<div id="topList" class="body"></div>
|
||||
</section>
|
||||
|
||||
<section class="pane bottom">
|
||||
<div class="hdr bot-cols">
|
||||
<span>Name</span>
|
||||
<span>Type</span>
|
||||
</div>
|
||||
<div id="bottomList" class="body"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div id="AcceptArea">
|
||||
<div id="export_btn" class="ButtonStyleConfirm ButtonTypeChoice" hidden>Export Presets</div>
|
||||
<div id="close_btn" class="ButtonStyleRegular ButtonTypeChoice">Close</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div id="ctxMenu" class="ctx" hidden>
|
||||
<button class="ctx-item" data-action="open_folder">Open folder in explorer</button>
|
||||
<button id = "delete_btn" class="ctx-item-delete" data-action="delete_bundle" hidden>Delete bundle</button>
|
||||
<button id = "unsubscribe_btn" class="ctx-item-subscribed" data-action="unsubscribe_bundle" hidden>Unsubscribe bundle</button>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
375
resources/web/dialog/PresetBundleDialog/index.js
Normal file
375
resources/web/dialog/PresetBundleDialog/index.js
Normal file
@@ -0,0 +1,375 @@
|
||||
// ========= Data Stores =========
|
||||
const bundlesById = new Map(); // bundleId -> bundle object
|
||||
const printersByBundle = new Map(); // bundleId -> Map(index -> printerName)
|
||||
const filamentsByBundle = new Map(); // bundleId -> Map(index -> filamentName)
|
||||
const presetsByBundle = new Map(); // bundleId -> Map(index -> presetName)
|
||||
const UPDATE_TOOLTIP = "Update available";
|
||||
const UNAUTHORIZED_TOOLTIP = "Unauthorized bundle";
|
||||
|
||||
// ========= DOM =========
|
||||
let topList = null;
|
||||
let bottomList = null;
|
||||
|
||||
let ctxMenu = null;
|
||||
let contextRow = null;
|
||||
let ctxMenuSubscribed = null;
|
||||
|
||||
let ctxMenuDelete = null;
|
||||
|
||||
let selectedBundleId = null;
|
||||
|
||||
// ========= Init =========
|
||||
function OnInit() {
|
||||
|
||||
topList = document.getElementById("topList");
|
||||
bottomList = document.getElementById("bottomList");
|
||||
ctxMenu = document.getElementById("ctxMenu");
|
||||
ctxMenuSubscribed = document.getElementById("unsubscribe_btn");
|
||||
ctxMenuDelete = document.getElementById("delete_btn");
|
||||
const closeBtn = document.getElementById("close_btn");
|
||||
const exportbtn = document.getElementById("export_btn");
|
||||
const refreshBtn = document.getElementById("refresh_btn");
|
||||
const autoUpdateToggle = document.getElementById("auto_update_toggle");
|
||||
|
||||
if (!topList || !bottomList) return;
|
||||
TranslatePage();
|
||||
|
||||
// If wx side needs to request bundles after page load:
|
||||
RequestBundles();
|
||||
|
||||
|
||||
refreshBtn?.addEventListener("click", () => {
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "refresh_bundles"
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
});
|
||||
|
||||
autoUpdateToggle?.addEventListener("change", () => {
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "set_auto_update",
|
||||
enabled: !!autoUpdateToggle.checked
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
});
|
||||
// Hook selection on top list
|
||||
topList.addEventListener("click", (e) => {
|
||||
const cloudLink = e.target.closest(".bundle-cloud-link");
|
||||
if (cloudLink) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const row = cloudLink.closest(".row");
|
||||
if (!row) return;
|
||||
|
||||
selectTopRow(row);
|
||||
selectedBundleId = String(row.dataset.id || "");
|
||||
renderBottomForBundle(selectedBundleId);
|
||||
sendOpenBundleOnCloud(selectedBundleId);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateBtn = e.target.closest(".bundle-update-btn");
|
||||
if (updateBtn) {
|
||||
e.stopPropagation();
|
||||
if (updateBtn.disabled) return;
|
||||
|
||||
const row = updateBtn.closest(".row");
|
||||
if (!row) return;
|
||||
|
||||
selectTopRow(row);
|
||||
selectedBundleId = String(row.dataset.id || "");
|
||||
renderBottomForBundle(selectedBundleId);
|
||||
sendUpdateBundleCommand(selectedBundleId);
|
||||
return;
|
||||
}
|
||||
|
||||
const row = e.target.closest(".row");
|
||||
if (!row) return;
|
||||
|
||||
selectTopRow(row);
|
||||
selectedBundleId = String(row.dataset.id || "");
|
||||
renderBottomForBundle(selectedBundleId);
|
||||
});
|
||||
|
||||
// for top list rows if right click open context menu
|
||||
topList.addEventListener("contextmenu", (e) => {
|
||||
const row = e.target.closest(".row");
|
||||
if (!row) return; // top rows only
|
||||
|
||||
const bundleType = String(row.dataset.bundleType || "").toLowerCase();
|
||||
if (bundleType !== "subscribed") return;
|
||||
|
||||
e.preventDefault();
|
||||
selectTopRow(row);
|
||||
contextRow = row;
|
||||
showSubscribedMenu(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
// for top list rows except subscribed if right click open regular context menu
|
||||
topList.addEventListener("contextmenu", (e) => {
|
||||
const row = e.target.closest(".row");
|
||||
if (!row) return; // top rows only
|
||||
const bundleType = String(row.dataset.bundleType || "").toLowerCase();
|
||||
if (bundleType === "subscribed") return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
selectTopRow(row);
|
||||
contextRow = row;
|
||||
showMenu(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
ctxMenu?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-action]");
|
||||
if (!btn || !contextRow) return;
|
||||
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "top_row_menu_action",
|
||||
action: String(btn.dataset.action || ""),
|
||||
bundle_id: String(contextRow.dataset.id || "")
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
hideMenu();
|
||||
});
|
||||
|
||||
closeBtn?.addEventListener("click", () => {
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "close_page"
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
});
|
||||
|
||||
exportbtn?.addEventListener("click", () => {
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "export_page"
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest(".ctx")) hideMenu();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") hideMenu();
|
||||
});
|
||||
}
|
||||
// ========= wx bridge requests =========
|
||||
|
||||
function RequestBundles() {
|
||||
var tSend={};
|
||||
tSend['sequence_id']=Math.round(new Date() / 1000);
|
||||
tSend['command']="request_bundles";
|
||||
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
function HandleStudio(pVal) {
|
||||
|
||||
const msg = (typeof pVal === "string") ? safeJsonParse(pVal) : pVal;
|
||||
if (!msg || typeof msg !== "object") return;
|
||||
|
||||
const strCmd = msg.command;
|
||||
if (strCmd === "list_bundles") {
|
||||
unpackPayload(msg);
|
||||
renderTop();
|
||||
// auto-select first bundle if none selected
|
||||
autoSelectFirstBundle();
|
||||
|
||||
const autoUpdateToggle = document.getElementById("auto_update_toggle");
|
||||
if (autoUpdateToggle) {
|
||||
autoUpdateToggle.checked = !!msg.auto_update_enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========= Parse / store =========
|
||||
function unpackPayload(payload) {
|
||||
bundlesById.clear();
|
||||
printersByBundle.clear();
|
||||
filamentsByBundle.clear();
|
||||
presetsByBundle.clear();
|
||||
|
||||
const list = payload?.data || [];
|
||||
for (const bundle of list) {
|
||||
const id = String(bundle.id ?? "");
|
||||
if (!id) continue;
|
||||
|
||||
bundlesById.set(id, {
|
||||
id,
|
||||
name: bundle.name ?? "",
|
||||
type: bundle.type ?? "",
|
||||
version: bundle.version ?? "",
|
||||
path: bundle.path ?? "",
|
||||
update_available: Boolean(bundle.update_available) ,
|
||||
unauthorized: Boolean(bundle.unauthorized)
|
||||
});
|
||||
|
||||
printersByBundle.set(id, new Map((bundle.printers || []).map((name, i) => [i, name])));
|
||||
filamentsByBundle.set(id, new Map((bundle.filaments || []).map((name, i) => [i, name])));
|
||||
presetsByBundle.set(id, new Map((bundle.presets || []).map((name, i) => [i, name])));
|
||||
}
|
||||
}
|
||||
|
||||
// ========= Render: top =========
|
||||
function renderTop() {
|
||||
const bundles = Array.from(bundlesById.values());
|
||||
|
||||
topList.innerHTML = bundles.map(b => `
|
||||
<div class="row" data-id="${escapeAttr(b.id)}" data-bundle-type="${escapeAttr(String(b.type || "").toLowerCase())}">
|
||||
<div class="cell bundle-name-cell" title="${escapeAttr(b.name)}">
|
||||
${b.unauthorized
|
||||
? `<span class="bundle-status-icon bundle-status-icon-unauthorized" title="${escapeAttr(UNAUTHORIZED_TOOLTIP)}" aria-label="${escapeAttr(UNAUTHORIZED_TOOLTIP)}">!</span>`
|
||||
: b.update_available
|
||||
? `<span class="bundle-status-icon bundle-status-icon-update" title="${escapeAttr(UPDATE_TOOLTIP)}" aria-label="${escapeAttr(UPDATE_TOOLTIP)}">↑</span>`
|
||||
: `<span class="bundle-status-icon-spacer" aria-hidden="true"></span>`}
|
||||
${
|
||||
b.type === "Subscribed" ?
|
||||
`<a href="#" class="bundle-name-text bundle-cloud-link" title="Open this bundle in your browser">${escapeHtml(b.name)}</a>`
|
||||
: `<span class="bundle-name-text">${escapeHtml(b.name)}</span>`
|
||||
}
|
||||
</div>
|
||||
<span title="${escapeAttr(b.type)}">${escapeHtml(b.type)}</span>
|
||||
<span title="${escapeAttr(b.version)}">${escapeHtml(b.version)}</span>
|
||||
<div class="cell bundle-update-cell">
|
||||
<button
|
||||
type="button"
|
||||
class="bundle-update-btn ${(!b.unauthorized && b.update_available) ? "is-enabled" : "is-disabled"}"
|
||||
${(!b.unauthorized && b.update_available) ? "" : "disabled"}
|
||||
data-id="${escapeAttr(b.id)}"
|
||||
>Update</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function sendOpenBundleOnCloud(bundleId) {
|
||||
const bundle = bundlesById.get(String(bundleId || ""));
|
||||
if (!bundle) return;
|
||||
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "open_bundle_on_cloud",
|
||||
bundle_id: String(bundle.id || "")
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
function sendUpdateBundleCommand(bundleId) {
|
||||
const bundle = bundlesById.get(String(bundleId || ""));
|
||||
if (!bundle || bundle.unauthorized || !bundle.update_available) return;
|
||||
|
||||
const tSend = {
|
||||
sequence_id: Math.round(Date.now() / 1000),
|
||||
command: "update_bundle",
|
||||
bundle_id: String(bundle.id || "")
|
||||
};
|
||||
SendWXMessage(JSON.stringify(tSend));
|
||||
}
|
||||
|
||||
// ========= Render: bottom (for a selected bundle) =========
|
||||
function renderBottomForBundle(bundleId) {
|
||||
const key = String(bundleId || "");
|
||||
const printers = printersByBundle.get(key) || new Map();
|
||||
const filaments = filamentsByBundle.get(key) || new Map();
|
||||
const presets = presetsByBundle.get(key) || new Map();
|
||||
|
||||
// Convert to a flat list of rows { typeLabel, name }
|
||||
const rows = [];
|
||||
|
||||
for (const [, name] of printers) rows.push({ type: "Printer", name });
|
||||
for (const [, name] of filaments) rows.push({ type: "Filament", name });
|
||||
for (const [, name] of presets) rows.push({ type: "Preset", name });
|
||||
|
||||
bottomList.innerHTML = rows.map((r, idx) => `
|
||||
<div class="row" data-id="${escapeAttr(bundleId)}" data-idx="${idx}">
|
||||
<span>${escapeHtml(r.name)}</span>
|
||||
<span title="${escapeAttr(r.type)}">${escapeHtml(r.type)}</span>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// ========= Selection helpers =========
|
||||
function clearSelection() {
|
||||
document.querySelectorAll(".row.selected").forEach(r => r.classList.remove("selected"));
|
||||
}
|
||||
|
||||
function selectTopRow(rowEl) {
|
||||
// only clear selection in top list, not bottom
|
||||
topList.querySelectorAll(".row.selected").forEach(r => r.classList.remove("selected"));
|
||||
rowEl.classList.add("selected");
|
||||
}
|
||||
|
||||
function autoSelectFirstBundle() {
|
||||
if (selectedBundleId && bundlesById.has(selectedBundleId)) {
|
||||
// reselect existing
|
||||
const el = topList.querySelector(`.row[data-id="${cssEscape(selectedBundleId)}"]`);
|
||||
if (el) selectTopRow(el);
|
||||
renderBottomForBundle(selectedBundleId);
|
||||
return;
|
||||
}
|
||||
|
||||
const first = topList.querySelector(".row");
|
||||
if (!first) {
|
||||
bottomList.innerHTML = "";
|
||||
selectedBundleId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
selectTopRow(first);
|
||||
selectedBundleId = first.dataset.id;
|
||||
renderBottomForBundle(selectedBundleId);
|
||||
}
|
||||
|
||||
function showSubscribedMenu(x, y) {
|
||||
if (!ctxMenu) return;
|
||||
ctxMenu.style.left = `${x}px`;
|
||||
ctxMenu.style.top = `${y}px`;
|
||||
ctxMenu.hidden = false;
|
||||
ctxMenuDelete.hidden = true;
|
||||
ctxMenuSubscribed.hidden = false;
|
||||
}
|
||||
|
||||
function showMenu(x, y) {
|
||||
if (!ctxMenu) return;
|
||||
ctxMenu.style.left = `${x}px`;
|
||||
ctxMenu.style.top = `${y}px`;
|
||||
ctxMenu.hidden = false;
|
||||
ctxMenuDelete.hidden = false;
|
||||
ctxMenuSubscribed.hidden = true;
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
if (!ctxMenu) return;
|
||||
ctxMenu.hidden = true;
|
||||
ctxMenuSubscribed.hidden = true;
|
||||
contextRow = null;
|
||||
}
|
||||
// ========= Utilities =========
|
||||
function safeJsonParse(s) {
|
||||
try { return JSON.parse(s); } catch { return null; }
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
// minimal attribute escaping
|
||||
return escapeHtml(str);
|
||||
}
|
||||
|
||||
function cssEscape(str) {
|
||||
// basic css escape for attribute selectors
|
||||
return String(str).replaceAll('"', '\\"');
|
||||
}
|
||||
394
resources/web/dialog/PresetBundleDialog/styles.css
Normal file
394
resources/web/dialog/PresetBundleDialog/styles.css
Normal file
@@ -0,0 +1,394 @@
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--panel: #ffffff;
|
||||
--border: #d8d8d8;
|
||||
--border-strong: #e6e6e6;
|
||||
--border-soft: #f0f0f0;
|
||||
--col-sep: #e1e1e1;
|
||||
|
||||
--text: #1f2328;
|
||||
--row-hover: #f7f9fb;
|
||||
--row-selected: #eaf2ff;
|
||||
--row-selected-outline: #b7d0ff;
|
||||
|
||||
--footer-bg: #fafafa;
|
||||
--btn-bg: #ffffff;
|
||||
--btn-border: #cccccc;
|
||||
--btn-hover: #f0f0f0;
|
||||
|
||||
--ctx-bg: #ffffff;
|
||||
--ctx-border: #cccccc;
|
||||
--ctx-hover: #efefef;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* App layout: split content + footer */
|
||||
.app {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Split panes */
|
||||
.split {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1.25fr) minmax(140px, 0.75fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.auto-update-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auto-update-switch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auto-update-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auto-update-slider {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 24px;
|
||||
flex: 0 0 52px;
|
||||
border-radius: 999px;
|
||||
background: #b9b9b9;
|
||||
transition: background 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auto-update-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.auto-update-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.3px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auto-update-text-off {
|
||||
right: 7px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.auto-update-text-on {
|
||||
left: 7px;
|
||||
color: #ffffff;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.auto-update-switch input:checked + .auto-update-slider {
|
||||
background: var(--main-color);
|
||||
}
|
||||
|
||||
.auto-update-switch input:checked + .auto-update-slider::before {
|
||||
transform: translateX(28px);
|
||||
}
|
||||
|
||||
.auto-update-switch input:checked + .auto-update-slider .auto-update-text-on {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auto-update-switch input:checked + .auto-update-slider .auto-update-text-off {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.auto-update-switch input:not(:checked) + .auto-update-slider .auto-update-text-on {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.auto-update-switch input:not(:checked) + .auto-update-slider .auto-update-text-off {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auto-update-switch input:focus-visible + .auto-update-slider {
|
||||
outline: 2px solid var(--main-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.auto-update-switch input:disabled + .auto-update-slider {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pane {
|
||||
min-height: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.hdr {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--border-strong);
|
||||
padding: 8px 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.top-cols { grid-template-columns: minmax(0, 2fr) 1fr 1fr 88px; }
|
||||
.bot-cols { grid-template-columns: 2fr 1fr; }
|
||||
|
||||
.top .row { grid-template-columns: minmax(0, 2fr) 1fr 1fr 88px; }
|
||||
.bottom .row { grid-template-columns: 2fr 1fr; }
|
||||
|
||||
.body {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hdr > *,
|
||||
.row > * {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid var(--col-sep);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hdr > *:last-child,
|
||||
.row > *:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.bundle-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bundle-name-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bundle-cloud-link {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.bundle-cloud-link:hover,
|
||||
.bundle-cloud-link:focus-visible {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bundle-status-icon,
|
||||
.bundle-status-icon-spacer {
|
||||
flex: 0 0 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.bundle-status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f59e0b;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.bundle-status-icon-unauthorized {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.bundle-status-icon-update {
|
||||
background: var(--main-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bundle-update-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bundle-update-btn {
|
||||
box-sizing: border-box;
|
||||
min-width: 64px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.bundle-update-btn.is-enabled {
|
||||
background: var(--main-color);
|
||||
color: var(--button-fg-light);
|
||||
}
|
||||
|
||||
.bundle-update-btn.is-enabled:hover {
|
||||
background: var(--main-color-hover);
|
||||
}
|
||||
|
||||
.bundle-update-btn.is-disabled {
|
||||
background: var(--button-bg-disabled);
|
||||
color: var(--button-fg-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
#topList,
|
||||
#bottomList {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Column separators + clipping */
|
||||
|
||||
.row {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.row:hover { background: var(--row-hover); }
|
||||
|
||||
.row.selected {
|
||||
background: var(--row-selected);
|
||||
outline: 1px solid var(--row-selected-outline);
|
||||
}
|
||||
|
||||
/* Footer styling
|
||||
.footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 10px 16px;
|
||||
background: var(--footer-bg);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--btn-border);
|
||||
background: var(--btn-bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-btn:hover { background: var(--btn-hover); } */
|
||||
|
||||
/* Context menu */
|
||||
.ctx {
|
||||
position: fixed;
|
||||
min-width: 180px;
|
||||
background: var(--ctx-bg);
|
||||
border: 1px solid var(--ctx-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,.18);
|
||||
padding: 4px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ctx-item,
|
||||
.ctx-item-subscribed, .ctx-item-delete {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ctx-item:hover,
|
||||
.ctx-item-subscribed:hover ,
|
||||
.ctx-item-delete:hover
|
||||
{ background: var(--ctx-hover); }
|
||||
|
||||
.ctx-item[hidden],
|
||||
.ctx-item-subscribed[hidden] ,
|
||||
.ctx-item-delete[hidden]{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#AcceptArea {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
Reference in New Issue
Block a user