Destiny2_Term_replace

替换网页中出现的命运2术语

Versión del día 12/02/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Destiny2_Term_replace
// @namespace    your-namespace
// @version      2.0
// @description  替换网页中出现的命运2术语
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      20xiji.github.io
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/Destiny2_term.json';
    let replacementHistory = [];
    let termMap = new Map();
    let currentMode = 1;
    let dialogVisible = false;

    GM_addStyle(`
        #textReplacerDialog {
            position: fixed;
            top: 20px;
            right: 20px;
            background: #1a1a1a;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.25);
            z-index: 9999;
            width: 260px;
            font-family: Arial, sans-serif;
            color: #fff;
            display: none;
        }
        #modeButtons {
            display: grid;
            gap: 8px;
            margin: 12px 0;
        }
        .mode-btn {
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: #333;
            color: #888;
            cursor: pointer;
            transition: all 0.2s;
        }
        .mode-btn.active {
            background: #4CAF50;
            color: #fff;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }
        #actionButtons {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 12px;
        }
        #actionButtons button {
            flex: 1;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: #4CAF50;
            color: white;
            cursor: pointer;
            min-width: 80px;
        }
        #actionButtons button:disabled {
            background: #666;
            cursor: not-allowed;
        }
        #termCount {
            font-size: 12px;
            color: #888;
            margin-left: 8px;
        }
        #btnClearCache {
            background: #f44336 !important;
        }
    `);

    const dialog = document.createElement('div');
    dialog.id = 'textReplacerDialog';
    dialog.innerHTML = `
        <h3 style="margin:0 0 10px 0;font-size:16px;">文本替换工具 <span id="termCount">(加载中...)</span></h3>
        <div id="modeButtons">
            <button class="mode-btn" data-mode="1">中文模式</button>
            <button class="mode-btn" data-mode="2">英文|中文</button>
            <button class="mode-btn" data-mode="3">中文(英文)</button>
        </div>
        <div id="actionButtons">
            <button id="btnApplyAll">应用规则</button>
            <button id="btnUndo" disabled>撤销</button>
            <button id="btnClearCache">清除缓存</button>
        </div>
    `;
    document.body.appendChild(dialog);

    const elements = {
        modeButtons: dialog.querySelectorAll('.mode-btn'),
        btnApplyAll: dialog.querySelector('#btnApplyAll'),
        btnUndo: dialog.querySelector('#btnUndo'),
        btnClearCache: dialog.querySelector('#btnClearCache'),
        termCount: dialog.querySelector('#termCount')
    };

    elements.modeButtons.forEach(btn => btn.addEventListener('click', handleModeChange));
    elements.btnApplyAll.addEventListener('click', applyAllRules);
    elements.btnUndo.addEventListener('click', undoReplace);
    elements.btnClearCache.addEventListener('click', clearCache);

    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'k') {
            dialogVisible = !dialogVisible;
            dialog.style.display = dialogVisible ? 'block' : 'none';
            updateButtonStates();
        }
    });

    initTerminology();
    updateButtonStates();

    async function clearCache() {
        try {
            // 强制删除现有缓存
            GM_deleteValue('cachedTerms');
            GM_deleteValue('cacheTime');

            // 立即触发重新加载
            const freshData = await fetchTerms();
            termMap = new Map(Object.entries(freshData));
            GM_setValue('cachedTerms', freshData);
            GM_setValue('cacheTime', Date.now());

            updateTermCount();
            alert('✅ 缓存已清除并重新加载成功\n当前种类:武器、护甲、技能、模组\n已加载条目数:' + termMap.size);
        } catch (error) {
            console.error('缓存清除失败:', error);
            alert('❌ 缓存清除失败:' + error.message);
            termMap.clear();
            updateTermCount();
        }
    }

    async function initTerminology() {
        const CACHE_DAYS = 1;
        const cachedData = GM_getValue('cachedTerms');
        const cacheTime = GM_getValue('cacheTime', 0);

        try {
            // 缓存过期或不存在时强制更新
            if (!cachedData || Date.now() - cacheTime > 86400000 * CACHE_DAYS) {
                const freshData = await fetchTerms();
                termMap = new Map(Object.entries(freshData));
                GM_setValue('cachedTerms', freshData);
                GM_setValue('cacheTime', Date.now());
            } else {
                termMap = new Map(Object.entries(cachedData));
            }
        } catch (error) {
            console.error('术语表初始化失败:', error);
            if (cachedData) {
                termMap = new Map(Object.entries(cachedData));
            }
        }
        updateTermCount();
    }

    function updateTermCount() {
        elements.termCount.textContent = termMap.size > 0
            ? `(已加载${termMap.size}条)`
            : '(未加载数据)';
    }

    function fetchTerms() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: ITEM_LIST_URL,
                timeout: 15000,
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        try {
                            const data = JSON.parse(res.responseText);
                            // 修改这里,访问 data.data
                            if (data && data.data && Object.keys(data.data).length > 0) {
                                resolve(data.data); //  <--- 修改
                            } else {
                                reject(new Error('获取到空数据或data.data为空')); //  <--- 修改
                            }
                        } catch (e) {
                            reject(new Error('数据解析失败'));
                        }
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror: (err) => {
                    reject(new Error(`网络错误: ${err}`));
                },
                ontimeout: () => {
                    reject(new Error('请求超时(15秒)'));
                }
            });
        });
    }

    // 其他保持不变的功能函数
    function handleModeChange(e) {
        currentMode = parseInt(e.target.dataset.mode);
        updateButtonStates();
    }

    function updateButtonStates() {
        elements.modeButtons.forEach(btn => {
            btn.classList.toggle('active', parseInt(btn.dataset.mode) === currentMode);
        });
    }

    function applyAllRules() {
        const termRules = Array.from(termMap).map(([en, cn]) => {
            switch (currentMode) {
                case 1: return [en, cn];
                case 2: return [en, `${en} | ${cn}`];
                case 3: return [en, `${cn}(${en})`];
                default: return [en, cn];
            }
        });
        performReplace(termRules);
    }

    function performReplace(rules) {
        const regex = buildRegex(rules);
        const replaceMap = new Map(rules);
        const snapshot = [];

        const walker = document.createTreeWalker(
            document.body,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );

        while (walker.nextNode()) {
            const node = walker.currentNode;
            const original = node.nodeValue;
            const replaced = original.replace(regex, (m) => {
                const foundKey = Array.from(replaceMap.keys()).find(k =>
                    k.toLowerCase() === m.toLowerCase()
                );
                return foundKey ? replaceMap.get(foundKey) : m;
            });

            if (replaced !== original) {
                snapshot.push({ node, text: original });
                node.nodeValue = replaced;
            }
        }

        if (snapshot.length) {
            replacementHistory.push(snapshot);
            elements.btnUndo.disabled = false;
        }
    }

    function buildRegex(rules) {
        const sortedKeys = [...new Set(rules.map(([k]) => k))]
            .sort((a, b) => b.length - a.length)
            .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));

        return new RegExp(`\\b(${sortedKeys.join('|')})\\b`, 'gi');
    }

    function undoReplace() {
        if (replacementHistory.length) {
            const last = replacementHistory.pop();
            last.forEach(({ node, text }) => {
                if (node.parentNode) node.nodeValue = text;
            });
            elements.btnUndo.disabled = !replacementHistory.length;
        }
    }
})();