您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "Bambu Studio" button and replaces PrusaSlicer buttons on Printables.
// ==UserScript== // @name Printables Bambu Studio Button // @namespace http://wol.ph/ // @version 2025-10-03 // @description Adds a "Bambu Studio" button and replaces PrusaSlicer buttons on Printables. // @author wolph // @match https://www.printables.com/model/* // @run-at document-idle // @icon https://icons.duckduckgo.com/ip2/printables.com.ico // @license MIT // ==/UserScript== (() => { 'use strict'; // ------------------------------ // Config / constants // ------------------------------ const BAMBULAB_ICON = 'https://icons.duckduckgo.com/ip2/bambulab.com.ico'; const SLICE_SELECTOR = '.btn.slicer-download, button.slicer-download, .slicer-download'; // Track which buttons we've already patched const seen = new WeakSet(); // ------------------------------ // Utilities // ------------------------------ /** @param {Element} el */ const $closestCard = (el) => el.closest('.download-item'); /** Replace the icon(s) inside a Slice button with the Bambu favicon (don’t touch the text). */ function swapIcon(btn) { const imgs = btn.querySelectorAll('img'); if (imgs.length) { imgs.forEach((img) => { img.src = BAMBULAB_ICON; img.alt = 'Bambu Studio'; img.width = 14; img.height = 14; img.classList.add('bambu-icon'); img.style.borderRadius = '2px'; }); } else { // If there is no <img>, prepend one without changing text const img = document.createElement('img'); img.src = BAMBULAB_ICON; img.alt = 'Bambu Studio'; img.width = 14; img.height = 14; img.style.marginRight = '0.35rem'; img.style.verticalAlign = 'text-bottom'; img.className = 'bambu-icon'; btn.prepend(img); } // Tooltip only; do NOT modify visible text (prevents "Slice Slice") btn.title = 'Open in Bambu Studio'; } /** Remove duplicate "Slice" text nodes (some themes render two). Keep the first, blank out the rest. */ function dedupeSliceLabel(btn) { const textNodes = Array.from(btn.childNodes) .filter((n) => n.nodeType === Node.TEXT_NODE) .filter((n) => /\bSlice\b/i.test(n.textContent || '')); for (let i = 1; i < textNodes.length; i++) { textNodes[i].textContent = ''; // blank out extras } } /** Patch one Slice button: swap icon + de-dupe label (idempotent). */ function patchSliceButton(btn) { if (seen.has(btn)) return; seen.add(btn); swapIcon(btn); dedupeSliceLabel(btn); } /** Scan current DOM for Slice buttons. */ function scan() { document.querySelectorAll(SLICE_SELECTOR).forEach((btn) => { if (!(btn instanceof HTMLElement)) return; // Only touch buttons that live inside a download-item card to avoid false positives if (!$closestCard(btn)) return; patchSliceButton(btn); }); } // ------------------------------ // Protocol / navigation rewriting // ------------------------------ /** * Rewrite any Prusa custom-protocol navigation to Bambu Studio. * - prusaslicer://open?file=... -> bambustudio://open?file=... * - prusa://open?url=... -> bambustudio://open?file=... * Falls back to returning the original URL if it’s not a Prusa link. * @param {string|URL} raw * @returns {string|URL} */ function rewritePrusaToBambu(raw) { try { if (raw == null) return raw; let s = String(raw); // Only handle prusa*/ custom schemes const m = s.match(/^prusa(?:slicer)?:\/\/([^#?\/]+)(.*)$/i); if (!m) return raw; // Normalize path to 'open' const path = (m[1] || 'open').toLowerCase() === 'slice' ? 'open' : 'open'; // Extract query (if present) let qs = ''; const qIndex = s.indexOf('?'); if (qIndex >= 0) qs = s.slice(qIndex + 1); // Map common param names to "file" const params = new URLSearchParams(qs); const knownKeys = ['file', 'url', 'path', 'u']; let fileUrl = ''; for (const k of knownKeys) { if (params.has(k)) { fileUrl = params.get(k) || ''; break; } } // If still empty, attempt raw decode of the entire query if (!fileUrl && qs) fileUrl = qs; // Build bambustudio deep link const out = `bambustudio://open?file=${encodeURIComponent(fileUrl)}`; return out; } catch { return raw; } } // Intercept <a> clicks to prusa*:// and rewrite to bambu document.addEventListener('click', (ev) => { const target = /** @type {HTMLElement|null} */ (ev.target instanceof Element ? ev.target : null); const anchor = target?.closest?.('a[href^="prusaslicer://"], a[href^="prusa://"]'); if (anchor) { const href = anchor.getAttribute('href'); if (href) { ev.preventDefault(); ev.stopPropagation(); const rewritten = rewritePrusaToBambu(href); // Open via location to trigger protocol handler window.location.href = String(rewritten); } } }, true); // Intercept programmatic window.open("prusaslicer://...") (function patchWindowOpen() { const original = window.open; window.open = function patchedOpen(url, name, specs, replace) { const rewritten = rewritePrusaToBambu(url); return original.call(this, rewritten, name, specs, replace); }; })(); // Intercept programmatic anchor.click() commonly used by sites to trigger downloads (function patchAnchorClick() { const OriginalClick = HTMLAnchorElement.prototype.click; HTMLAnchorElement.prototype.click = function patchedAnchorClick() { try { const href = this.getAttribute('href') || this.href || ''; // If site tries to open Prusa directly, rewrite to Bambu if (/^prusa(?:slicer)?:\/\//i.test(href)) { const rewritten = rewritePrusaToBambu(href); // Prefer navigating instead of default click to avoid duplicate handlers window.location.href = String(rewritten); return; } // If a direct model file (.3mf) is being "clicked" programmatically, use Bambu Studio if (/\.(?:3mf)(?:$|\?)/i.test(href) || /\/download\b/i.test(href)) { const deep = `bambustudio://open?file=${encodeURIComponent(href)}`; window.location.href = deep; return; } } catch { // ignore and fall through } return OriginalClick.call(this); }; })(); // Some frameworks use location.assign/replace — intercept those as well (function patchLocationMethods() { const proto = /** @type {Location} */ (window.location).__proto__ || Location.prototype; const origAssign = proto.assign; const origReplace = proto.replace; proto.assign = function patchedAssign(url) { const rewritten = rewritePrusaToBambu(url); return origAssign.call(this, rewritten); }; proto.replace = function patchedReplace(url) { const rewritten = rewritePrusaToBambu(url); return origReplace.call(this, rewritten); }; })(); // ------------------------------ // Observe dynamic content // ------------------------------ const mo = new MutationObserver(() => scan()); mo.observe(document.documentElement, { childList: true, subtree: true }); scan(); })();