Destiny2_Term_replace

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

当前为 2025-02-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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