Scryfall卡牌汉化

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

目前為 2024-10-31 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();