// ==UserScript== // @name Makerworld Enhancements // @description Enhancements for Makerworld website // @version 1.0.4 // @icon https://raw.githubusercontent.com/JMcrafter26/userscripts/main/makerworld-enhancements/icon.png?raw=true // // @author Cufiy (aka JMcrafter26) // @namespace https://github.com/JMcrafter26/userscripts // // @downloadURL https://raw.githubusercontent.com/JMcrafter26/userscripts/main/makerworld-enhancements/makerworld-enhancements.user.js // @updateURL https://raw.githubusercontent.com/JMcrafter26/userscripts/main/makerworld-enhancements/makerworld-enhancements.user.js // @supportURL https://github.com/JMcrafter26/userscripts/issues // @homepageURL https://github.com/JMcrafter26/userscripts/tree/main/makerworld-enhancements // // @license AGPL-3.0 // @copyright Copyright (C) 2025, Cufiy // // @match https://makerworld.com/* // @match https://makerworld.com.cn/* // // @run-at document-end // // ==/UserScript== /** * Makerworld Enhancements Userscript * * @see http://wiki.greasespot.net/API_reference * @see http://wiki.greasespot.net/Metadata_Block */ (function () { const logoSvg = ``; function getEnhancementOptions(context = 'card') { const options = [ { text: 'Search on Printables', icon: 'https://unpkg.com/lucide-static@latest/icons/search.svg', for: 'card,details', action: (contextData) => { const name = context === 'details' ? getModelNameFromPage() : getCardName(contextData); if (!name) { alert('Could not find design name.'); return; } const query = encodeURIComponent(name); window.open(`https://www.printables.com/search?q=${query}`, '_blank'); } }, { text: 'Search on Thingiverse', icon: 'https://unpkg.com/lucide-static@latest/icons/search.svg', for: 'card,details', action: (contextData) => { const name = context === 'details' ? getModelNameFromPage() : getCardName(contextData); if (!name) { alert('Could not find design name.'); return; } const query = encodeURIComponent(name); window.open(`https://www.thingiverse.com/search?q=${query}`, '_blank'); } }, { text: 'Open in Bambu Studio', icon: 'https://unpkg.com/lucide-static@latest/icons/box.svg', for: 'card', action: (contextData) => { const url = context === 'details' ? window.location.href : getCardUrl(contextData); if (!url) { alert('Could not find design URL.'); return; } getModelDetails(url).then(data => { if (!data) { alert('Could not fetch model details.'); return; } const printProfiles = data.pageProps?.design?.instances || []; if (!printProfiles || printProfiles.length === 0) { alert('No print profiles found for this model.'); return; } const firstProfileId = printProfiles[0].id; const modelSlug = getModelSlug(url); try { openInBambuStudio(firstProfileId, modelSlug); } catch (error) { console.error('Error opening in Bambu Studio:', error); alert('An error occurred while trying to open in Bambu Studio.'); } }); } }, { text: 'Download 3MF Model', icon: 'https://unpkg.com/lucide-static@latest/icons/download.svg', for: 'card', action: (contextData) => { const url = context === 'details' ? window.location.href : getCardUrl(contextData); if (!url) { alert('Could not find design URL.'); return; } getModelDetails(url).then(data => { if (!data) { alert('Could not fetch model details.'); return; } const printProfiles = data.pageProps?.design?.instances || []; if (!printProfiles || printProfiles.length === 0) { alert('No print profiles found for this model.'); return; } const firstProfileId = printProfiles[0].id; const modelSlug = getModelSlug(url); getDownloadUrl(firstProfileId, modelSlug).then(downloadUrl => { if (!downloadUrl) { alert('Could not get download URL for 3MF file.'); return; } window.open(downloadUrl, '_blank'); }); }); } } ]; // Filter options based on context return options.filter(option => { const contexts = option.for.split(',').map(c => c.trim()); return contexts.includes(context); }); } function isModelViewPage() { return /\/models\/[\w-]+/.test(window.location.pathname); } function getDesignCards() { return document.querySelectorAll('.js-design-card'); } function addButtonToDesignCard(card) { // Prevent duplicate buttons if (card.querySelector('.enhancement-btn')) { return; } // Mark card as processed card.setAttribute('data-enhanced', 'true'); const button = document.createElement('button'); button.className = 'enhancement-btn'; button.innerHTML = logoSvg; // check if cards first child is a div with a span inside const firstChild = card.firstElementChild; if (firstChild && firstChild.tagName.toLowerCase() === 'div' && firstChild.querySelector('span')) { button.classList.add('enhancement-btn-offset'); } button.title = 'Makerworld Enhancement'; card.classList.add('enhancement-card'); card.appendChild(button); addPopover(button, card); } function addPopover(enhanceBtn, card) { console.log('Adding popover to button'); // new popover for enhancement const popover = document.createElement('div'); popover.classList.add('enhancement-popover', 'MuiPaper-root', 'MuiPaper-elevation', 'MuiPaper-rounded', 'MuiPaper-elevation8', 'MuiPopover-paper', 'MuiMenu-paper', 'MuiMenu-paper', 'mw-css-kqqlx6'); const optionsList = document.createElement('ul'); optionsList.classList.add('enhancement-options-list'); const options = getEnhancementOptions('card'); options.forEach(option => { const listItem = document.createElement('li'); listItem.classList.add('enhancement-option-item'); listItem.innerHTML = `${option.icon ? `` : ''}${option.text}`; listItem.addEventListener('click', () => { option.action(card); document.body.removeChild(popover); }); optionsList.appendChild(listItem); }); popover.appendChild(optionsList); enhanceBtn.addEventListener('click', (e) => { console.log('Enhancement button clicked'); e.stopPropagation(); // remove existing popovers const existingPopovers = document.querySelectorAll('.enhancement-popover'); if (existingPopovers.length > 0) { existingPopovers.forEach(p => { if (p.parentNode) { p.parentNode.removeChild(p); } }); } card.appendChild(popover); }); } function getModelNameFromPage() { // Try to get from h1 tag const h1 = document.querySelector('h1'); if (h1) { return h1.innerText.trim(); } // Fallback to page title const title = document.title; if (title) { return title.split('|')[0].trim(); } return false; } function getCardName(card) { const titleElement = card.querySelector('.design-bottom-row .translated-text a'); if (titleElement) { return titleElement.innerText.trim(); } return false; } function getCardUrl(card) { const linkElement = card.querySelector('.design-bottom-row .translated-text a'); if (linkElement) { return linkElement.href; } return false; } function getModelSlug(url) { const match = url.match(/\/models\/([\w-]+)/); return match ? match[1] : null; } function getNextJSBuildId() { // get buildId from window.__NEXT_DATA__.buildId if available if (window.__NEXT_DATA__ && window.__NEXT_DATA__.buildId) { return window.__NEXT_DATA__.buildId; } // search on the entire html page for "buildId":"{buildId}" and return the buildId const html = document.documentElement.innerHTML; const match = html.match(/"buildId":"([\w\d]+)"/); return match ? match[1] : null; } function openInBambuStudio(printProfileId, modelSlug) { getDownloadUrl(printProfileId, modelSlug).then(downloadUrl => { if (!downloadUrl) { alert('Could not get download URL for F3MF file.'); return; } // split the downloadUrl by ? and add a space after the ? const [baseUrl, query] = downloadUrl.split('?'); const bambuStudioUrl = `bambustudio://open?file=${encodeURIComponent(baseUrl + (query ? '?' + query : ''))}`; try { console.log('Attempting to open Bambu Studio with URL:', bambuStudioUrl); window.location.href = bambuStudioUrl; } catch (error) { console.error('Error opening Bambu Studio:', error); alert('An error occurred while trying to open Bambu Studio. Please ensure Bambu Studio is installed.'); } }); } async function getDownloadUrl(printProfileId, modelSlug) { try { const response = await fetch(`https://makerworld.com/api/v1/design-service/instance/${printProfileId}/f3mf?type=download`, { "credentials": "include", "headers": { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0", "Accept": "*/*", "Accept-Language": "en-US;q=0.7,en;q=0.3", "X-BBL-Client-Type": "web", "X-BBL-Client-Version": "00.00.00.01", "X-BBL-App-Source": "makerworld", "X-BBL-Client-Name": "MakerWorld", "Content-Type": "application/json", "Sec-GPC": "1", "Alt-Used": "makerworld.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0" }, "referrer": `https://makerworld.com/en/models/${modelSlug}`, "method": "GET", "mode": "cors" }); if (response.ok) { console.log('F3MF file download response received.'); const data = await response.json(); if (data && data.url) { return data.url; } else if (data && data.code == 1 && data.captchaId) { alert('Captcha required to download this F3MF file. Please complete the captcha on the Makerworld website and try again.'); return null; } else { console.error('No download URL found in response data.'); return null; } } else { if (response.status === 418) { alert('Captcha required to download this F3MF file. Please complete the captcha on the Makerworld website and try again.'); return null; } alert('Failed to download F3MF file from Makerworld.'); } } catch (error) { console.error('Error downloading F3MF file:', error); alert('An error occurred while downloading the F3MF file.'); } } async function getModelDetails(url, modelId = null) { // if modelid is not set, get it from url: e.g. https://makerworld.com/de/models/1866618-wheel-loader-kit-card#profileId-1997183 --> 1866618 if (!modelId) { const match = url.match(/\/models\/(\d+)/); if (match) { modelId = match[1]; } } if (!modelId) { return null; } const moduleSlug = getModelSlug(url); if (!moduleSlug) { return null; } console.log('Fetching model details for model ID:', modelId); console.log('Using module slug:', moduleSlug); let fetchUrl = `https://makerworld.com/_next/data/${getNextJSBuildId()}/de/models/${moduleSlug}.json?designId=${moduleSlug}`; try { const response = await fetch(fetchUrl, { "credentials": "include", "headers": { "User-Agent": window.navigator.userAgent, "Accept": "*/*", "Accept-Language": "en-US;q=0.7,en;q=0.3", "x-nextjs-data": "1", "Sec-GPC": "1", "Alt-Used": "makerworld.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Priority": "u=0" }, "referrer": "https://makerworld.com/de", "method": "GET", "mode": "cors" }); const data = await response.json(); console.log('Fetched model details:', data); return data; } catch (error) { console.error('Error fetching model details:', error); return null; } } function addButtonGroupToModelView() { // Find the stats div const statsDiv = document.querySelector('.mw-css-pn2l0k'); if (!statsDiv || statsDiv.hasAttribute('data-enhancement-added')) { return; } statsDiv.setAttribute('data-enhancement-added', 'true'); const buttonGroup = document.createElement('div'); buttonGroup.className = 'enhancement-button-group'; const buttons = getEnhancementOptions('details'); buttons.forEach(btn => { const button = document.createElement('button'); button.className = 'enhancement-model-btn'; button.innerHTML = `${btn.icon ? `` : ''}${btn.text}`; button.addEventListener('click', () => btn.action(null)); buttonGroup.appendChild(button); }); // Insert after the stats div statsDiv.parentNode.insertBefore(buttonGroup, statsDiv.nextSibling); } function injectCSS() { const style = document.createElement('style'); style.innerHTML = ` .enhancement-card { position: relative; } .enhancement-btn { background: transparent; border: none; cursor: pointer; position: absolute; top: 0px; left: 0px; z-index: 1000; width: 32px; height: 32px; padding: 4px; border-radius: 0 0 3px 0; color: #b0fd41; background-color: rgba(0, 0, 0); } .enhancement-btn-offset { left: 32px; } .enhancement-popover { position: absolute; top: 36px; left: 0; z-index: 1001; width: 200px; height: auto; max-height: 300px; padding: 8px 0; background-color: rgb(45, 45, 49); border: 0.9px solid rgb(82, 82, 82); border-radius: .35em; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); color: rgb(239, 239, 240); overflow-y: auto; transition: opacity 0.211s cubic-bezier(0.4, 0, 0.2, 1), transform 0.141s cubic-bezier(0.4, 0, 0.2, 1); } .enhancement-options-list { margin: 0; padding: 0; list-style: none; } .enhancement-option-item { padding: 4px 16px 4px 16px; cursor: pointer; display: flex; align-items: center; user-select: none; border-radius: 2px; font-family: "Open Sans", "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue"; font-size: 14px; font-weight: 600; } .enhancement-option-item:hover { background-color: rgba(52, 53, 58, 1); } .enhancement-option-icon { display: inline-block; width: 16px; height: 16px; background-size: contain; background-repeat: no-repeat; vertical-align: middle; margin-right: 8px; filter: invert(1); } .enhancement-button-group { display: flex; align-items: center; height: 46px; margin-top: 16px; flex-wrap: wrap; border: 1px solid rgb(82, 82, 82); border-radius: 4px; background-color: transparent; } .enhancement-model-btn { height: 46px; background-color: transparent; border: transparent; padding: 0 12px; margin: 0; border-right: 1px solid rgb(82, 82, 82); color: rgb(239, 239, 240); cursor: pointer; font-size: 14px; transition: background-color 0.2s ease; } .enhancement-model-btn:hover { background-color: rgba(52, 53, 58, 1); } .enhancement-model-btn img { width: 16px; height: 16px; filter: invert(1); } `; document.head.appendChild(style); console.log('Injected custom CSS for enhancement popover.'); } function addMutationObserver() { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { // Check the node itself if (node.nodeType === 1) { if (node.classList && node.classList.contains('js-design-card') && !node.hasAttribute('data-enhanced')) { addButtonToDesignCard(node); } // Also check child nodes (for nested structures) const childCards = node.querySelectorAll && node.querySelectorAll('.js-design-card'); if (childCards && childCards.length > 0) { childCards.forEach(card => { if (!card.hasAttribute('data-enhanced')) { addButtonToDesignCard(card); } }); } // Check for model view stats div if (isModelViewPage()) { const statsDiv = node.querySelector && node.querySelector('.mw-css-pn2l0k'); if (statsDiv && !statsDiv.hasAttribute('data-enhancement-added')) { addButtonGroupToModelView(); } } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } function enhanceMakerworld() { injectCSS(); if (isModelViewPage()) { // On model view page, add button group addButtonGroupToModelView(); } else { // On other pages, add buttons to design cards const designCards = getDesignCards(); designCards.forEach(card => { addButtonToDesignCard(card); }); } addMutationObserver(); // Single event listener for closing popovers (event delegation) document.addEventListener('click', (e) => { // Don't close if clicking on enhancement button if (e.target.closest('.enhancement-btn')) { return; } const existingPopovers = document.querySelectorAll('.enhancement-popover'); existingPopovers.forEach(p => { if (p.parentNode) { p.parentNode.removeChild(p); } }); }); } console.log('Makerworld Enhancements by Cufiy loaded.'); enhanceMakerworld(); })();