Makerworld Enhancements

Enhancements for Makerworld website

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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) <https://cufiy.net>
// @namespace      https://github.com/JMcrafter26/userscripts
//
// @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 = `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles-icon lucide-sparkles"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>`;

    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 ? `<img src="${option.icon}" class="enhancement-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 ? `<img src="${btn.icon}" class="enhancement-option-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();
})();