// ==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();
}
})();