Destiny2_Term_replace

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

// ==UserScript==
// @name         Destiny2_Term_replace
// @namespace    your-namespace
// @version      2.3
// @description  替换网页中出现的命运2术语
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @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;
    let dialogXOffset = 0;
    let dialogYOffset = 0;
    let isDragging = false;
    let posObjs = [];
    let hintDialogVisible = 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;
            overflow: visible;
        }
        #textReplacerDialog.dragging {
            cursor: grabbing;
        }
        #dialogHeader {
            cursor: grab;
            margin-bottom: 10px;
        }
        #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;
        }
        .dialogButton { /* 统一关闭和提示按钮样式 */
            position: absolute;
            top: 8px;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background-color: #ff6058;
            border: 1px solid #e0443e;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 1px 0 rgba(0,0,0,.1);
            padding: 0;
            z-index: 10000;
        }
        .dialogButton:hover {
            background-color: #f0413a;
            border-color: #d02828;
        }
        .dialogButton::before {
            content: '';
            display: block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background-color: #fff;
            transform: scale(0.5); /* 调整小白点初始大小 */
            opacity: 0;
            transition: opacity 0.2s ease, transform 0.2s ease; /* 添加transform过渡 */
        }
        .dialogButton:hover::before {
            opacity: 1;
            transform: scale(1);
        }
        #dialogCloseButton {
            right: 8px;
        }
        #dialogHintButton {
            right: 30px; /* 提示按钮位置在关闭按钮左侧 */
            background-color: #ffc107; /* 提示按钮颜色 */
            border-color: #e0a300;
        }
        #dialogHintButton:hover {
            background-color: #f0b200;
            border-color: #d09500;
        }
        #dialogHintButton:hover::before {
            background-color: #333; /* 提示按钮悬停小白点颜色 */
        }
        #hintDialog {
            position: fixed;
            top: 60px; /* 调整提示框的垂直位置 */
            right: 20px;
            background: #333;
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.25);
            z-index: 10001; /* 确保提示框在最上层 */
            width: 300px; /* 调整宽度 */
            font-size: 14px;
            line-height: 1.6;
            display: none; /* 初始隐藏 */
        }
        #hintDialog p {
            margin-bottom: 10px;
        }
        #hintDialog p:last-child {
            margin-bottom: 0;
        }
    `);

    const dialog = document.createElement('div');
    dialog.id = 'textReplacerDialog';

    const dialogHeader = document.createElement('div');
    dialogHeader.id = 'dialogHeader';
    dialogHeader.style.margin = '0 0 10px 0';
    dialogHeader.style.fontSize = '16px';
    dialogHeader.textContent = '文本替换工具 ';
    dialog.appendChild(dialogHeader);

    const termCountSpan = document.createElement('span');
    termCountSpan.id = 'termCount';
    termCountSpan.textContent = '(加载中...)';
    dialogHeader.appendChild(termCountSpan);

    const modeButtonsDiv = document.createElement('div');
    modeButtonsDiv.id = 'modeButtons';

    const modeButton1 = document.createElement('button');
    modeButton1.className = 'mode-btn';
    modeButton1.dataset.mode = '1';
    modeButton1.textContent = '中文模式';
    modeButtonsDiv.appendChild(modeButton1);

    const modeButton2 = document.createElement('button');
    modeButton2.className = 'mode-btn';
    modeButton2.dataset.mode = '2';
    modeButton2.textContent = '英文|中文';
    modeButtonsDiv.appendChild(modeButton2);

    const modeButton3 = document.createElement('button');
    modeButton3.className = 'mode-btn';
    modeButton3.dataset.mode = '3';
    modeButton3.textContent = '中文(英文)';
    modeButtonsDiv.appendChild(modeButton3);


    const actionButtonsDiv = document.createElement('div');
    actionButtonsDiv.id = 'actionButtons';

    const btnApplyAll = document.createElement('button');
    btnApplyAll.id = 'btnApplyAll';
    btnApplyAll.textContent = '应用规则';
    actionButtonsDiv.appendChild(btnApplyAll);

    const btnUndo = document.createElement('button');
    btnUndo.id = 'btnUndo';
    btnUndo.textContent = '撤销';
    btnUndo.disabled = true;
    actionButtonsDiv.appendChild(btnUndo);

    const btnClearCache = document.createElement('button');
    btnClearCache.id = 'btnClearCache';
    btnClearCache.textContent = '清除缓存';
    actionButtonsDiv.appendChild(btnClearCache);

    const closeButton = document.createElement('button');
    closeButton.id = 'dialogCloseButton';
    closeButton.className = 'dialogButton'; // 添加统一样式类
    closeButton.addEventListener('click', toggleDialog);
    dialog.appendChild(closeButton);

    // 新增提示按钮
    const hintButton = document.createElement('button');
    hintButton.id = 'dialogHintButton';
    hintButton.className = 'dialogButton'; // 添加统一样式类
    hintButton.addEventListener('click', toggleHintDialog); // 添加点击事件监听器
    dialog.appendChild(hintButton);

    // 创建提示对话框
    const hintDialog = document.createElement('div');
    hintDialog.id = 'hintDialog';
    hintDialog.textContent = `
        <p>网页多层嵌套操作说明:</p>
        <p>当处理采用多层嵌套结构的网页时,系统表现如下特点:</p>
        <ol>
            <li>非快捷键触发场景<br>
            当用户使用不使用快捷键调用功能面板时,由于网页存在多层嵌套,系统会同时激活两个功能面板。这两个面板各自对应不同层级网页的替换操作需求。</li>
            <li>快捷键触发场景<br>
            当用户使用快捷键调用功能面板时,系统会根据当前鼠标点击位置智能判定目标层级,此时呼出的面板仅作用于用户当前操作的网页层级。<br>
            (说明:网页结构的多层嵌套特性导致了不同触发方式下的面板响应差异,自动触发会启动全量面板,而快捷键触发则是上下文感知的精准响应)</li>
        </ol>
    `;
    document.body.appendChild(hintDialog);


    dialog.appendChild(modeButtonsDiv);
    dialog.appendChild(actionButtonsDiv);

    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') {
            toggleDialog();
        }
    });

    GM_registerMenuCommand("打开文本替换工具", toggleDialog);

    document.addEventListener('click', (e) => {
        if (e.target.matches('.gm-open-text-replacer')) {
            toggleDialog();
        }
    });

    // Make dialog draggable
    dialogHeader.addEventListener('mousedown', dragStart);
    document.addEventListener('mousemove', dragMove);
    document.addEventListener('mouseup', dragEnd);

    function dragStart(e) {
        isDragging = true;
        dialog.classList.add('dragging');
        dialogXOffset = dialog.offsetLeft - e.clientX;
        dialogYOffset = dialog.offsetTop - e.clientY;
    }

    function dragMove(e) {
        if (!isDragging) return;
        dialog.style.left = e.clientX + dialogXOffset + 'px';
        dialog.style.top = e.clientY + dialogYOffset + 'px';
    }

    function dragEnd() {
        isDragging = false;
        dialog.classList.remove('dragging');
    }


    initTerminology();
    updateButtonStates();

    function toggleDialog() {
        dialogVisible = !dialogVisible;
        dialog.style.display = dialogVisible ? 'block' : 'none';
        updateButtonStates();
        if (dialogVisible && hintDialogVisible) { // 关闭主面板时同时关闭提示框
            toggleHintDialog();
        }
    }

    function toggleHintDialog() {
        hintDialogVisible = !hintDialogVisible;
        hintDialog.style.display = hintDialogVisible ? 'block' : 'none';
        if (hintDialogVisible && dialogVisible === false) { // 如果提示框显示时主面板未显示,则同时显示主面板
            toggleDialog();
        }
    }

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址