Scryfall卡牌汉化

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

当前为 2024-10-31 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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