Convert MakerWorld points to RMB display for both CN and international sites
// ==UserScript==
// @name MakerWorld Points to RMB Converter
// @name:zh-CN MakerWorld积分转人民币转换器
// @namespace http://tampermonkey.net/
// @version 0.9.1
// @description Convert MakerWorld points to RMB display for both CN and international sites
// @description:zh-CN 支持国际站和中文站的MakerWorld积分自动转换人民币显示
// @author AIScripter
// @match https://makerworld.com.cn/*
// @match https://makerworld.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const EXCHANGE_RATES = {
'normal': 0.5795, // Normal points exchange rate
'exclusive': 0.47 // Exclusive points exchange rate
};
let isUpdating = false; // 防止重复执行的标志
// Function to parse number text with internationalization support
function parseNumber(text) {
// Handle text with <em> tags for decimal parts
if (text.includes('<em')) {
const mainPart = text.split('<em')[0].trim();
const decimalMatch = text.match(/\.<!-- -->(\d+)/);
const decimal = decimalMatch ? decimalMatch[1] : '';
return parseFloat(mainPart.replace(/,/g, '') + '.' + decimal);
}
// Handle regular numbers
return parseFloat(text.replace(/,/g, ''));
}
// Function to convert points to RMB
function convertPointsToRMB(points, type = 'normal') {
const rate = EXCHANGE_RATES[type] || EXCHANGE_RATES.normal;
return Math.round(points * rate);
}
// Function to create or update RMB display element
function createOrUpdateRMBDisplay(container, value, className, styles = {}) {
let rmbDisplay = container.querySelector(`.${className}`);
if (!rmbDisplay) {
rmbDisplay = document.createElement('span');
rmbDisplay.className = className;
rmbDisplay.setAttribute('data-rmb-converter', 'true'); // 标记为脚本创建的元素
Object.assign(rmbDisplay.style, {
color: '#666',
marginLeft: '4px',
display: 'inline-block',
verticalAlign: 'baseline',
...styles
});
container.appendChild(rmbDisplay);
}
rmbDisplay.textContent = `≈ ${value} 元`;
return rmbDisplay;
}
// Function to update points display
function updatePointsDisplay() {
if (isUpdating) return; // 如果正在更新,直接返回
isUpdating = true;
try {
// Handle points type display area (both CN and international versions)
const pointTypesContainer = document.querySelector('.mw-css-dx018d');
if (pointTypesContainer) {
const typeBlocks = pointTypesContainer.querySelectorAll('[class*="mw-css-"][class*="gc1l0t"]');
typeBlocks.forEach(block => {
const pointsSpan = block.querySelector('[class*="mw-css-"][class*="yyek0l"]');
const typeLabel = block.querySelector('[class*="mw-css-"][class*="kx5qaq"]');
if (pointsSpan && typeLabel) {
const pointsText = pointsSpan.innerHTML;
const pointsValue = parseNumber(pointsText);
const isExclusive = typeLabel.textContent.includes('独家') ||
typeLabel.textContent.toLowerCase().includes('exclusive');
const rmbValue = convertPointsToRMB(pointsValue, isExclusive ? 'exclusive' : 'normal');
createOrUpdateRMBDisplay(
block.querySelector('[class*="mw-css-"][class*="gr0cu"]'),
rmbValue,
'rmb-type-conversion',
{ fontSize: '12px' }
);
}
});
}
// Handle total points display area
const totalPointsContainers = document.querySelectorAll('[class*="mw-css-"][class*="gjjkf7"]');
totalPointsContainers.forEach(container => {
const pointsSpan = container.querySelector('[class*="mw-css-"][class*="yyek0l"]');
if (pointsSpan) {
const pointsText = pointsSpan.innerHTML;
const pointsValue = parseNumber(pointsText);
const rmbValue = convertPointsToRMB(pointsValue);
createOrUpdateRMBDisplay(
container,
rmbValue,
'rmb-total-conversion',
{ fontSize: '14px', textAlign: 'center' }
);
}
});
// Handle small version points display (including nested formats)
const smallPointsContainers = document.querySelectorAll('[class*="mw-css-"][class*="foh4ep"]');
smallPointsContainers.forEach(container => {
// Try both direct and nested yyek0l span structures
const pointsSpan = container.querySelector('[class*="mw-css-"][class*="yyek0l"]');
const nestedPointsSpan = container.querySelector('[class*="mw-css-"][class*="1541sxf"] [class*="mw-css-"][class*="yyek0l"]');
const targetSpan = pointsSpan || nestedPointsSpan;
if (targetSpan) {
const pointsText = targetSpan.innerHTML;
const pointsValue = parseNumber(pointsText);
const rmbValue = convertPointsToRMB(pointsValue);
createOrUpdateRMBDisplay(
container,
rmbValue,
'rmb-individual-conversion',
{ fontSize: '0.75em', textAlign: 'center' }
);
}
});
// Handle income/expenditure details
const incomeLabels = document.querySelectorAll('[class*="mw-css-"][class*="jrs7sa"]');
incomeLabels.forEach(label => {
const incomeSpans = label.querySelectorAll('[class*="mw-css-"][class*="jgoohm"] [class*="mw-css-"][class*="yyek0l"]');
incomeSpans.forEach((span, index) => {
const pointsText = span.innerHTML;
const pointsValue = parseNumber(pointsText);
const rmbValue = convertPointsToRMB(pointsValue);
const parentContainer = span.closest('[class*="mw-css-"][class*="jgoohm"]');
createOrUpdateRMBDisplay(
parentContainer,
rmbValue,
`rmb-${index === 0 ? 'income' : 'expenditure'}-conversion`,
{ fontSize: '0.75em' }
);
});
});
} finally {
isUpdating = false; // 确保标志被重置
}
}
// 延迟执行函数,防止频繁调用
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 创建防抖版本的更新函数
const debouncedUpdate = debounce(updatePointsDisplay, 300);
// Add page load event and mutation observer
window.addEventListener('load', function() {
updatePointsDisplay();
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
// 检查变化是否与我们的脚本无关
for (let mutation of mutations) {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE &&
!node.hasAttribute('data-rmb-converter') &&
!node.querySelector('[data-rmb-converter]')) {
shouldUpdate = true;
break;
}
}
if (shouldUpdate) break;
}
if (shouldUpdate) {
debouncedUpdate();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
// 添加页面可见性变化时的更新
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
setTimeout(updatePointsDisplay, 100);
}
});
})();