Greasy Fork is available in English.

Blendermarket Downloader

Added CGDownload and GFXCamp buttons for downloading paid items from Blendermarket.

// ==UserScript==
// @name         Blendermarket Downloader
// @description  Added CGDownload and GFXCamp buttons for downloading paid items from Blendermarket.
// @icon         https://assets.superhivemarket.com/site_assets/images/black_bee.png
// @version      1.4
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://blendermarket.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    let observer;
    let buttonCheckInterval;
    let retryCount = 0;
    const MAX_RETRIES = 10;
    const RETRY_DELAY = 500;

    const ICONS = {
        cgdownload: 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/blendermarket/cgdownload.png',
        gfxcamp: 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/blendermarket/gfxcamp.png'
    };

    function addStyles() {
        const styles = `
            .download-btn {
                background: linear-gradient(120deg, #6800f0, #ff6b00);
                color: white;
                border: none;
                padding: 4px;
                border-radius: 4px;
                cursor: pointer;
                position: absolute;
                transition: all 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 28px;
                height: 28px;
            }
            
            .download-btn:hover {
                background: linear-gradient(120deg, #5600c7, #e65d00);
            }

            .download-btn.cgdownload-btn {
                right: 45px;
                bottom: 15px;
            }

            .download-btn.gfxcamp-btn {
                right: 12px;
                bottom: 15px;
            }

            .card-body {
                position: relative;
            }

            .download-btn img {
                width: 16px;
                height: 16px;
                object-fit: contain;
            }
        `;
        
        const styleSheet = document.createElement('style');
        styleSheet.textContent = styles;
        document.head.appendChild(styleSheet);
    }

    function getProductNameFromURL() {
        const currentURL = window.location.href;
        const match = currentURL.match(/products\/([^/?]+)/);
        return match ? match[1] : '';
    }

    function getProductNameFromLink(cardBody) {
        const cardProduct = cardBody.closest('.card.card-product');
        if (cardProduct) {
            const link = cardProduct.querySelector('a[href*="/products/"]');
            if (link) {
                const match = link.href.match(/products\/([^/?]+)/);
                return match ? match[1] : '';
            }
        }
        
        const link = cardBody.querySelector('a[href*="/products/"]');
        if (link) {
            const match = link.href.match(/products\/([^/?]+)/);
            return match ? match[1] : '';
        }
        
        return '';
    }

    function createCGDownloadURL(productName) {
        return `https://cgdownload.ru/?s=${encodeURIComponent(productName.replace(/-/g, ' '))}`;
    }

    function createGFXCampURL(productName) {
        return `https://www.gfxcamp.com/${productName}/`;
    }

    function clearExistingButtons() {
        const existingButtons = document.querySelectorAll('.download-btn, .cgdownload-button, .gfxcamp-button');
        existingButtons.forEach(button => button.remove());
    }

    function createCardButton(type, text, urlCreator, iconUrl, className) {
        const button = document.createElement('button');
        button.className = `download-btn ${className}`;
        
        const icon = document.createElement('img');
        icon.src = iconUrl;
        icon.alt = text;
        
        button.appendChild(icon);
        
        return button;
    }

    function addCardButtons() {
        const cardBodies = document.querySelectorAll('.card.card-product .card-body');
        
        cardBodies.forEach(cardBody => {
            if (!cardBody.closest('.card.card-product')) return;
            
            const existingButtons = cardBody.querySelectorAll('.download-btn');
            existingButtons.forEach(button => button.remove());

            const cgDownloadButton = createCardButton(
                'cgdownload',
                'CGDownload',
                createCGDownloadURL,
                ICONS.cgdownload,
                'cgdownload-btn'
            );

            const gfxCampButton = createCardButton(
                'gfxcamp',
                'GFXCamp',
                createGFXCampURL,
                ICONS.gfxcamp,
                'gfxcamp-btn'
            );

            cgDownloadButton.addEventListener('click', (e) => {
                e.preventDefault();
                const productName = getProductNameFromLink(cardBody);
                if (productName) {
                    window.open(createCGDownloadURL(productName), '_blank');
                }
            });

            gfxCampButton.addEventListener('click', (e) => {
                e.preventDefault();
                const productName = getProductNameFromLink(cardBody);
                if (productName) {
                    window.open(createGFXCampURL(productName), '_blank');
                }
            });

            cardBody.appendChild(cgDownloadButton);
            cardBody.appendChild(gfxCampButton);
        });
    }

    function createButton(className, text, urlCreator, iconUrl) {
        const originalButton = document.querySelector('.button_to input[type="submit"]');
        if (!originalButton) return null;

        const button = document.createElement('button');
        button.className = originalButton.className;
        button.classList.add(className);
        
        if (originalButton.style.cssText) {
            button.style.cssText = originalButton.style.cssText;
        }

        const contentWrapper = document.createElement('div');
        contentWrapper.style.display = 'flex';
        contentWrapper.style.alignItems = 'center';
        contentWrapper.style.justifyContent = 'center';
        contentWrapper.style.gap = '8px';

        const icon = document.createElement('img');
        icon.src = iconUrl;
        icon.alt = text;
        icon.style.width = '20px';
        icon.style.height = '20px';
        icon.style.objectFit = 'contain';

        const textSpan = document.createElement('span');
        textSpan.textContent = text;

        contentWrapper.appendChild(icon);
        contentWrapper.appendChild(textSpan);
        button.appendChild(contentWrapper);

        button.addEventListener('click', function(e) {
            e.preventDefault();
            const productName = getProductNameFromURL();
            if (productName) {
                const downloadURL = urlCreator(productName);
                window.open(downloadURL, '_blank');
            }
        });

        return button;
    }

    function addProductPageButtons() {
        if (!window.location.href.includes('/products/')) {
            return;
        }

        clearExistingButtons();

        const originalForm = document.querySelector('.button_to');
        if (!originalForm) {
            return;
        }

        if (document.querySelector('.cgdownload-button') && document.querySelector('.gfxcamp-button')) {
            return;
        }

        const priceElement = document.querySelector('.js-price-cart');
        if (priceElement) {
            priceElement.classList.remove('d-none', 'd-md-block');
            priceElement.classList.add('text-center');
            priceElement.style.marginBottom = '1rem';
            priceElement.style.display = 'block';
            priceElement.style.width = '100%';
            originalForm.parentNode.insertBefore(priceElement, originalForm);
        }

        const cgDownloadButton = createButton(
            'cgdownload-button',
            'CGDownload',
            createCGDownloadURL,
            ICONS.cgdownload
        );

        const gfxCampButton = createButton(
            'gfxcamp-button',
            'GFXCamp',
            createGFXCampURL,
            ICONS.gfxcamp
        );

        if (cgDownloadButton && gfxCampButton) {
            const wrapper = document.createElement('div');
            wrapper.style.marginTop = '0.5rem';
            wrapper.appendChild(cgDownloadButton);
            
            const wrapper2 = document.createElement('div');
            wrapper2.style.marginTop = '0.5rem';
            wrapper2.appendChild(gfxCampButton);

            originalForm.insertAdjacentElement('afterend', wrapper2);
            originalForm.insertAdjacentElement('afterend', wrapper);
        }
    }

    function addAllButtons() {
        addStyles();
        addProductPageButtons();
        addCardButtons();
    }

    function startButtonCheck() {
        if (buttonCheckInterval) {
            clearInterval(buttonCheckInterval);
        }

        retryCount = 0;
        buttonCheckInterval = setInterval(() => {
            if (addAllButtons() || retryCount >= MAX_RETRIES) {
                clearInterval(buttonCheckInterval);
                buttonCheckInterval = null;
                retryCount = 0;
            } else {
                retryCount++;
            }
        }, RETRY_DELAY);
    }

    function startObserver() {
        if (observer) {
            observer.disconnect();
        }

        startButtonCheck();

        observer = new MutationObserver((mutations) => {
            const hasRelevantChanges = mutations.some(mutation => {
                const addedNodes = Array.from(mutation.addedNodes);
                return addedNodes.some(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        return node.querySelector('.button_to, .card-body') || 
                               node.classList.contains('button_to', 'card-body') ||
                               node.closest('.button_to, .card-body');
                    }
                    return false;
                });
            });

            if (hasRelevantChanges) {
                startButtonCheck();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'style'],
            characterData: false
        });
    }

    function setupHistoryListener() {
        const pushState = history.pushState;
        history.pushState = function() {
            pushState.apply(history, arguments);
            setTimeout(startObserver, 100);
        };

        const replaceState = history.replaceState;
        history.replaceState = function() {
            replaceState.apply(history, arguments);
            setTimeout(startObserver, 100);
        };

        window.addEventListener('popstate', () => setTimeout(startObserver, 100));
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            setupHistoryListener();
            startObserver();
        });
    } else {
        setupHistoryListener();
        startObserver();
    }
})();