Scryfall卡牌汉化

为Scryfall没有中文的卡牌添加汉化,所有汉化数据均来自中文卡查sbwsz.com

// ==UserScript==
// @name         Scryfall卡牌汉化
// @description  为Scryfall没有中文的卡牌添加汉化,所有汉化数据均来自中文卡查sbwsz.com
// @author       lieyanqzu
// @license      GPL
// @namespace    http://github.com/lieyanqzu
// @icon         https://scryfall.com/favicon.ico
// @version      1.2
// @match        *://scryfall.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .print-langs-item.translate-toggle {
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -khtml-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }
    `);

    const API_BASE_URL = 'https://api.sbwsz.com/card';
    const TYPE_NAME_TRANSLATIONS_URL = 'https://sbwsz.com/static/typeName.json';
    let typeNameTranslations = null;
    const cardLanguageStates = new Map();

    GM_registerMenuCommand('默认显示中文: ' + (GM_getValue('defaultToChinese', false) ? '开' : '关'), toggleDefaultLanguage);

    function getCardInfoFromDOM(cardProfile) {
        const printsCurrentSet = cardProfile.querySelector('.prints-current-set');
        if (!printsCurrentSet) return null;
        
        const setMatch = printsCurrentSet.href.match(/\/sets\/(\w+)/);
        if (!setMatch) return null;
        const setCode = setMatch[1];
        
        const detailsText = cardProfile.querySelector('.prints-current-set-details')?.textContent || '';
        const numberMatch = detailsText.match(/#([^\s]+)/);
        if (!numberMatch) return null;
        const collectorNumber = numberMatch[1];
        
        return { setCode, collectorNumber };
    }

    document.addEventListener('contextmenu', function(e) {
        if (e.target.classList.contains('translate-toggle')) {
            e.preventDefault();
            e.stopPropagation();
            const parent = e.target.closest('[data-card-id]') || document;
            const cardInfo = getCardInfoFromDOM(parent);
            if (cardInfo) {
                setTimeout(() => {
                    const sbwszUrl = `https://sbwsz.com/card/${cardInfo.setCode}/${cardInfo.collectorNumber}`;
                    GM_openInTab(sbwszUrl, false);
                }, 0);
            }
            return false;
        }
    }, {capture: true, passive: false});

    function toggleDefaultLanguage() {
        const newDefault = !GM_getValue('defaultToChinese', false);
        GM_setValue('defaultToChinese', newDefault);
        location.reload();
    }

    async function getChineseCardData(setCode, collectorNumber) {
        const apiUrl = `${API_BASE_URL}/${setCode}/${collectorNumber}`;
        try {
            const response = await makeRequest('GET', apiUrl);
            const data = JSON.parse(response.responseText);
            const scryfallFaceCount = document.querySelectorAll('.card-text-title').length || 1;

            if (scryfallFaceCount === 1) {
                return processSingleFacedCard(data.data[0]);
            } else if (data.type === 'double' && data.data.length === 2) {
                return processDoubleFacedCard(data.data);
            } else if (data.type === 'normal' && data.data.length > 0) {
                return processSingleFacedCard(data.data[0]);
            }
            throw new Error('无法取中文卡牌数据');
        } catch (error) {
            console.error('获取中文卡牌数据失败:', error);
            throw error;
        }
    }

    function processCardFace(cardData) {
        const name = cardData.zhs_faceName || cardData.translatedName || cardData.zhs_name || cardData.officialName || cardData.name;
        return {
            name,
            text: processText(cardData.translatedText || cardData.zhs_text || cardData.officialText || cardData.text, name),
            flavorText: processText(cardData.zhs_flavorText || cardData.translatedFlavorText || cardData.flavorText)
        };
    }

    const processDoubleFacedCard = data => ({
        front: processCardFace(data[0]),
        back: processCardFace(data[1])
    });

    const processSingleFacedCard = cardData => processCardFace(cardData);

    function processText(text, cardName) {
        if (!text) return text;
        text = text.replace(/\\n/g, '\n');
        return cardName ? text.replace(/CARDNAME/g, cardName) : text;
    }

    async function getTypeNameTranslations() {
        if (typeNameTranslations) return typeNameTranslations;
        try {
            const response = await makeRequest('GET', TYPE_NAME_TRANSLATIONS_URL);
            typeNameTranslations = JSON.parse(response.responseText);
            return typeNameTranslations;
        } catch (error) {
            console.error('获取类别翻译数据失败:', error);
            throw error;
        }
    }

    async function translateType(englishType) {
        const translations = await getTypeNameTranslations();
        return englishType.trim().split('—').map((part, index) => {
            const words = part.trim().split(/\s+/);
            const translatedWords = words.map(word => translations[word] || word);
            return index === 0 ? translatedWords.join('') : translatedWords.join('/');
        }).join(' ~ ');
    }

    async function main() {
        const cardProfiles = document.querySelectorAll('.card-profile');
        
        const containers = cardProfiles.length > 0 ? cardProfiles : [document];

        for (const container of containers) {
            const cardInfo = getCardInfoFromDOM(container);
            if (!cardInfo) continue;
            
            const cardId = `${cardInfo.setCode}_${cardInfo.collectorNumber}`;
            container.dataset.cardId = cardId;
            
            try {
                saveOriginalContent(container);
                addToggleButton(true, container);

                const chineseData = await getChineseCardData(cardInfo.setCode, cardInfo.collectorNumber);
                const scryfallFaceCount = container.querySelectorAll('.card-text-title').length || 1;

                if (scryfallFaceCount === 1 || !chineseData.front) {
                    await saveSingleFacedCard(chineseData, container);
                } else {
                    await saveDoubleFacedCard(chineseData, container);
                }

                updateToggleButton(false, container);

                const defaultToChinese = GM_getValue('defaultToChinese', false);
                cardLanguageStates.set(cardId, false);
                if (defaultToChinese) {
                    await toggleLanguage({ 
                        preventDefault: () => {}, 
                        target: container.querySelector('.print-langs-item') 
                    }, cardId);
                }
            } catch (error) {
                console.error('处理卡牌时出错:', error);
                updateToggleButton(true, container);
            }
        }
    }

    function addToggleButton(loading = false, parent = document) {
        const printLangs = parent.querySelector('.print-langs');
        if (!printLangs) return;

        const cardId = parent.dataset.cardId || document.location.pathname;
        if (!parent.dataset.cardId) {
            parent.dataset.cardId = cardId;
        }

        const toggleLink = document.createElement('a');
        toggleLink.className = 'print-langs-item translate-toggle';
        toggleLink.href = 'javascript:void(0);';
        toggleLink.textContent = loading ? '加载中...' : (cardLanguageStates.get(cardId) ? '原文' : '汉化');
        toggleLink.style.cursor = loading ? 'wait' : 'pointer';
        
        if (!loading) {
            toggleLink.addEventListener('click', (e) => toggleLanguage(e, cardId));
        }

        printLangs.insertBefore(toggleLink, printLangs.firstChild);
    }

    function updateToggleButton(error = false, parent = document) {
        const toggleLink = parent.querySelector('.print-langs-item');
        if (toggleLink) {
            const cardId = parent.dataset.cardId || document.location.pathname;
            toggleLink.textContent = error ? '加载失败' : (cardLanguageStates.get(cardId) ? '原文' : '汉化');
            toggleLink.style.cursor = error ? 'not-allowed' : 'pointer';
            if (error) {
                toggleLink.removeEventListener('click', (e) => toggleLanguage(e, cardId));
            } else {
                toggleLink.addEventListener('click', (e) => toggleLanguage(e, cardId));
            }
        }
    }

    async function toggleLanguage(event, cardId) {
        event.preventDefault();
        
        const cardContainer = cardId === document.location.pathname 
            ? document 
            : document.querySelector(`[data-card-id="${cardId}"]`);
        
        if (!cardContainer) {
            console.error('找不到卡牌容器');
            return;
        }

        const elements = cardContainer.querySelectorAll(
            '.card-text-card-name, .card-text-type-line, .card-text-oracle, .card-text-flavor, ' +
            '.card-legality-item dt, .card-legality-item dd'
        );
        const toggleLink = event.target;

        if (elements.length === 0 || !elements[0].dataset.chineseContent) {
            console.error('中文数据尚未加载完成');
            return;
        }

        const currentState = cardLanguageStates.get(cardId) || false;
        cardLanguageStates.set(cardId, !currentState);
        toggleLink.textContent = cardLanguageStates.get(cardId) ? '原文' : '汉化';

        elements.forEach(el => {
            if (el.dataset.chineseContent) {
                [el.innerHTML, el.dataset.chineseContent] = [el.dataset.chineseContent, el.innerHTML];
            }
        });
    }

    function saveOriginalContent(parent = document) {
        parent.querySelectorAll('.card-text-card-name, .card-text-type-line, .card-text-oracle, .card-text-flavor').forEach(el => {
            el.dataset.originalContent = el.innerHTML;
        });
    }

    async function saveSingleFacedCard(chineseData, parent = document) {
        await saveCardFace(parent, parent, chineseData, 0);
        console.log('中文数据已保存');
    }

    async function saveDoubleFacedCard(chineseData, parent = document) {
        const cardTextDiv = parent.querySelector('.card-text');
        if (!cardTextDiv) {
            console.error('无法找到卡牌文本元素');
            return;
        }

        const cardFaces = cardTextDiv.querySelectorAll('.card-text-title');
        if (cardFaces.length !== 2) {
            console.error('无法找到双面卡牌的元素');
            return;
        }

        await Promise.all([
            saveCardFace(cardTextDiv, cardFaces[0], chineseData.front, 0),
            saveCardFace(cardTextDiv, cardFaces[1], chineseData.back, 1)
        ]);
    }

    async function saveCardFace(cardTextDiv, cardFace, faceData, faceIndex) {
        await Promise.all([
            saveElementText('.card-text-card-name', faceData.name, cardFace),
            saveType(cardTextDiv.querySelectorAll('.card-text-type-line')[faceIndex], faceData.name),
            saveCardText('.card-text-oracle', faceData.text, cardTextDiv, faceIndex),
            faceData.flavorText ? saveCardText('.card-text-flavor', faceData.flavorText, cardTextDiv, faceIndex) : Promise.resolve(),
            saveLegality(cardTextDiv)
        ]);
    }

    async function saveElementText(selector, text, parent = document) {
        const element = parent.querySelector(selector);
        if (element) {
            element.dataset.chineseContent = text;
        }
    }

    async function saveType(typeLineElement, cardName) {
        if (!typeLineElement) return;
        const colorIndicator = typeLineElement.querySelector('.color-indicator');
        const typeText = typeLineElement.textContent.replace(colorIndicator ? colorIndicator.textContent.trim() : '', '').trim();

        try {
            const translatedType = await translateType(typeText);
            typeLineElement.dataset.chineseContent = colorIndicator
                ? `${colorIndicator.outerHTML} ${translatedType}`
                : translatedType;
        } catch (error) {
            console.error('翻译类型时出错:', error);
        }
    }

    async function saveCardText(selector, text, parent = document, index = 0) {
        const elements = parent.querySelectorAll(selector);
        if (elements[index]) {
            const preservedHtml = await preserveManaSymbols(elements[index].innerHTML, text);
            elements[index].dataset.chineseContent = `<p>${preservedHtml.replace(/\n/g, '</p><p>')}</p>`;
        }
    }

    async function preserveManaSymbols(originalHtml, chineseText) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = originalHtml;
        const manaSymbols = tempDiv.querySelectorAll('abbr.card-symbol');

        const symbolMap = new Map();
        manaSymbols.forEach(symbol => {
            const symbolText = symbol.title.match(/\{(.+?)\}/)?.[0] || symbol.textContent;
            symbolMap.set(symbolText, (symbolMap.get(symbolText) || []).concat(symbol.outerHTML));
        });

        return Array.from(symbolMap).reduce((result, [symbolText, htmls]) => {
            let index = 0;
            return result.replace(new RegExp(escapeRegExp(symbolText), 'g'), () => {
                const html = htmls[index];
                index = (index + 1) % htmls.length;
                return html;
            });
        }, chineseText);
    }

    const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

    function makeRequest(method, url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method,
                url,
                onload: resolve,
                onerror: reject
            });
        });
    }

    const FORMAT_TRANSLATIONS = {
        'Standard': '标准',
        'Alchemy': '炼金',
        'Pioneer': '先驱',
        'Explorer': '探险',
        'Modern': '摩登',
        'Historic': '史迹',
        'Legacy': '薪传',
        'Brawl': '争锋',
        'Vintage': '特选',
        'Timeless': '永恒',
        'Commander': '指挥官',
        'Pauper': '纯铁',
        'Oathbreaker': '破誓',
        'Penny': '便士'
    };

    const LEGALITY_TRANSLATIONS = {
        'Legal': '合法',
        'Not Legal': '不合法',
        'Banned': '禁用',
        'Restrict.': '限制'
    };

    async function saveLegality(parent = document) {
        const legalityItems = parent.querySelectorAll('.card-legality-item');
        legalityItems.forEach(item => {
            const format = item.querySelector('dt');
            const legality = item.querySelector('dd');
            
            if (format && legality) {
                const formatText = format.textContent.trim();
                format.dataset.chineseContent = FORMAT_TRANSLATIONS[formatText] || formatText;
                
                const legalityText = legality.textContent.trim();
                legality.dataset.chineseContent = LEGALITY_TRANSLATIONS[legalityText] || legalityText;
            }
        });
    }

    main();
})();