Scryfall卡牌汉化

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

Od 17.09.2024.. Pogledajte najnovija verzija.

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

(function() {
    'use strict';

    const API_BASE_URL = 'https://api.sbwsz.com/card';
    const TYPE_NAME_TRANSLATIONS_URL = 'https://sbwsz.com/static/typeName.json';
    let typeNameTranslations = null;
    let isChineseDisplayed = GM_getValue('defaultToChinese', false);

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

    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 match = window.location.pathname.match(/\/card\/(\w+)\/([^/]+)\//);
        if (!match) {
            console.error('无法从 URL 获取 setCode 和 collectorNumber');
            return;
        }

        const [, setCode, collectorNumber] = match;

        try {
            saveOriginalContent();
            addToggleButton(true);

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

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

            updateToggleButton();

            if (isChineseDisplayed) {
                isChineseDisplayed = false;
                await toggleLanguage({ preventDefault: () => {}, target: document.querySelector('.print-langs-item') }, false);
            }
        } catch (error) {
            console.error('获取或保存中文数据时出错:', error);
            updateToggleButton(true);
        }
    }

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

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

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

    function updateToggleButton(error = false) {
        const toggleLink = document.querySelector('.print-langs-item');
        if (toggleLink) {
            toggleLink.textContent = error ? '加载失败' : (isChineseDisplayed ? '原文' : '汉化');
            toggleLink.style.cursor = error ? 'not-allowed' : 'pointer';
            toggleLink[error ? 'removeEventListener' : 'addEventListener']('click', toggleLanguage);
        }
    }

    async function toggleLanguage(event, updateButton = true) {
        event.preventDefault();
        const elements = document.querySelectorAll('.card-text-card-name, .card-text-type-line, .card-text-oracle, .card-text-flavor');
        const toggleLink = event.target;

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

        isChineseDisplayed = !isChineseDisplayed;

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

        if (updateButton) {
            toggleLink.textContent = isChineseDisplayed ? '原文' : '汉化';
        }
    }

    function saveOriginalContent() {
        document.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) {
        await saveCardFace(document, document, chineseData, 0);
        console.log('中文数据已保存');
    }

    async function saveDoubleFacedCard(chineseData) {
        const cardTextDiv = document.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()
        ]);
    }

    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
            });
        });
    }

    main();
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。