MakerWorld Points to RMB Converter

Convert MakerWorld points to RMB display for both CN and international sites

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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