Greasy Fork

来自缓存

Greasy Fork is available in English.

Destiny2_Term_replace

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Destiny2_Term_replace
// @namespace    your-namespace
// @version      2.4
// @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 CACHE_DAYS = 1;            // 词库缓存天数
    const HISTORY_LIMIT = 20;        // 撤销记录上限
    const DIALOG_POS_KEY = 'dialogPos'; // 面板位置存储键
    const USER_TERMS_KEY = 'userDefinedTerms'; // 自定义术语存储键
    const ITEMS_PER_PAGE_KEY = 'itemsPerPageSetting';

    const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/Destiny2_term.json';
    let replacementHistory = [];
    let termMap = new Map();
    let userTerms = {}; // 持久化的自定义术语
    let currentMode = 1;
    let dialogVisible = false;
    let dialogXOffset = 0;
    let dialogYOffset = 0;
    let isDragging = false;
    let posObjs = [];
    let hintDialogVisible = false; // 新增提示对话框显示状态

    let currentPage = 1;
    let itemsPerPage = GM_getValue(ITEMS_PER_PAGE_KEY, 5);
    let searchTerm = '';

    GM_addStyle(`
        :root {
            --bg-color:#1f1f1f;
            --accent-color:#4caf50;
            --accent-color-light:#66bb6a;
            --btn-bg:#333;
            --text-color:#fff;
            --text-muted:#888;
        }
        @keyframes gm-fadein {from{opacity:0;transform:translateY(-8px);}to{opacity:1;}}
        #textReplacerDialog{background:var(--bg-color);color:var(--text-color);animation:gm-fadein .25s ease-out;}
        .mode-btn{background:var(--btn-bg);color:var(--text-muted);} .mode-btn:hover{background:#444;color:var(--text-color);} .mode-btn.active{background:var(--accent-color);color:var(--text-color);}
        #actionButtons button{background:var(--accent-color);} #actionButtons button:hover{background:var(--accent-color-light);} 
        #btnClearCache{background:#f44336!important;} #btnClearCache:hover{background:#e53935!important;}
        #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;
        }
        /* Toast 提示样式 */
        .gm-toast {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.8);
            color: #fff;
            padding: 10px 16px;
            border-radius: 4px;
            font-size: 14px;
            z-index: 10002;
            opacity: 0;
            transition: opacity .3s ease;
            pointer-events: none; /* 不阻挡鼠标操作 */
        }

        /* 批量添加术语面板(嵌入主对话框) */
        #addTermPanel {
            margin-top: 12px;
            display: none;
        }
        #addTermPanel textarea {
            width: 100%;
            height: 100px;
            background: #222;
            color: #fff;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 6px;
            resize: vertical;
            box-sizing: border-box;
            font-family: monospace;
        }
        #addTermPanel .panel-actions {
            text-align: right;
            margin-top: 6px;
        }
        #addTermPanel .panel-actions button {
            margin-left: 8px;
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #btnSaveTerms {background:#4CAF50;color:#fff;}
        #btnCancelAdd {background:#666;color:#fff;}
    `);

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

    /* ===== 读取并应用历史面板位置 ===== */
    const savedPos = GM_getValue(DIALOG_POS_KEY);
    if (savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number') {
        dialog.style.left = `${savedPos.x}px`;
        dialog.style.top  = `${savedPos.y}px`;
    }

    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 = '中文模式';
    modeButton1.title = '将英文术语替换为纯中文';
    modeButtonsDiv.appendChild(modeButton1);

    const modeButton2 = document.createElement('button');
    modeButton2.className = 'mode-btn';
    modeButton2.dataset.mode = '2';
    modeButton2.textContent = '英文|中文';
    modeButton2.title = '替换为 "英文 | 中文" 组合';
    modeButtonsDiv.appendChild(modeButton2);

    const modeButton3 = document.createElement('button');
    modeButton3.className = 'mode-btn';
    modeButton3.dataset.mode = '3';
    modeButton3.textContent = '中文(英文)';
    modeButton3.title = '替换为 "中文(英文)" 组合';
    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 btnAddTerm = document.createElement('button');
    btnAddTerm.id = 'btnAddTerm';
    btnAddTerm.textContent = '添加术语';
    actionButtonsDiv.appendChild(btnAddTerm);

    const btnExportTerms = document.createElement('button');
    btnExportTerms.id = 'btnExportTerms';
    btnExportTerms.textContent = '导出';
    actionButtonsDiv.appendChild(btnExportTerms);

    const btnImportTerms = document.createElement('button');
    btnImportTerms.id = 'btnImportTerms';
    btnImportTerms.textContent = '导入';
    actionButtonsDiv.appendChild(btnImportTerms);

    /* === 新增:管理自定义术语按钮 === */
    const btnManageTerms = document.createElement('button');
    btnManageTerms.id = 'btnManageTerms';
    btnManageTerms.textContent = '管理术语';
    actionButtonsDiv.appendChild(btnManageTerms);

    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.innerHTML = `
        <h3 style="margin:0 0 8px 0;">使用小贴士</h3>
        <ul style="padding-left:20px;line-height:1.7">
          <li><b>启动模式:</b>按 <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>K</kbd> 或 点击右上角按钮 或 鼠标右键菜单可随时打开/关闭面板</li>
          <li><b>批量添加:</b>点「添加术语」后粘贴多行 <code>英文=中文</code> 或 <code>英文 中文</code> 映射即可导入</li>
          <li><b>撤销:</b>点击“撤销”按钮可回退最近 20 次替换操作</li>
          <li><b>自定义词库:</b>使用「导出 / 导入」按钮可备份和恢复自定义术语</li>
          <li><b>缓存:</b>如词库异常,可点“清除缓存”重新下载最新数据</li>
          <li><b>拖拽面板:</b>按住标题栏拖动可移动面板,位置会自动保存</li>
          <li><b>多层网页:</b>在 iframe 层内单击空白处后按快捷键,只替换当前层</li>
        </ul>
    `;
    document.body.appendChild(hintDialog);

    /* === 批量添加术语面板(在主面板内部) === */
    const addTermPanel = document.createElement('div');
    addTermPanel.id = 'addTermPanel';
    addTermPanel.innerHTML = `
        <p style="font-size:12px;color:#bbb;margin:0 0 6px;line-height:1.4;">
            <b>批量添加说明:</b>每行一条,英文与中文之间可使用 <code>=</code> 或 <code>|</code> 分隔。<br>
            例如:<br>
            <code>Gjallarhorn=加拉尔号角</code><br>
            <code>Gjallarhorn|加拉尔号角</code>
        </p>
        <textarea id="batchTermInput" placeholder="在此粘贴或输入多行术语映射..."></textarea>
        <div class="panel-actions">
            <button id="btnSaveTerms">保存</button>
            <button id="btnCancelAdd">取消</button>
        </div>`;
    // 先添加模式与操作区域,再放置批量添加面板,保证面板在最下方向下展开
    dialog.appendChild(modeButtonsDiv);
    dialog.appendChild(actionButtonsDiv);
    dialog.appendChild(addTermPanel);

    /* === 新增:管理自定义术语面板 === */
    const manageTermPanel = document.createElement('div');
    manageTermPanel.id = 'manageTermPanel';
    manageTermPanel.style.display = 'none';
    manageTermPanel.innerHTML = `
        <h4 style="margin:0 0 6px 0;">我的自定义术语</h4>
        <div style="margin-bottom:6px;display:flex;align-items:center;gap:4px;font-size:12px;flex-wrap:nowrap;">
            <input id="termSearchInput" type="text" placeholder="搜索..." style="flex:1 1 auto;min-width:0;background:#111;border:1px solid #555;border-radius:4px;padding:4px 6px;color:#fff;" />
            <label style="white-space:nowrap;">每页
                <input id="itemsPerPageInput" type="number" min="1" max="100" value="20" style="width:40px;margin:0 4px;background:#111;border:1px solid #555;border-radius:4px;padding:2px 4px;color:#fff;" />
                条
            </label>
        </div>
        <div id="termsList" style="max-height:160px;overflow:auto;border:1px solid #555;padding:6px;border-radius:4px;background:#222;"></div>
        <div id="paginationControls" style="margin-top:6px;text-align:center;font-size:12px;"></div>
        <div class="panel-actions" style="text-align:right;margin-top:6px;">
            <button id="btnCloseManage" style="background:#666;color:#fff;border:none;border-radius:4px;padding:6px 12px;cursor:pointer;">关闭</button>
        </div>`;
    dialog.appendChild(manageTermPanel);

    const batchInput = addTermPanel.querySelector('#batchTermInput');
    const btnSaveTerms = addTermPanel.querySelector('#btnSaveTerms');
    const btnCancelAdd = addTermPanel.querySelector('#btnCancelAdd');

    document.body.appendChild(dialog);

    const elements = {
        modeButtons: dialog.querySelectorAll('.mode-btn'),
        btnApplyAll: dialog.querySelector('#btnApplyAll'),
        btnUndo: dialog.querySelector('#btnUndo'),
        btnClearCache: dialog.querySelector('#btnClearCache'),
        btnAddTerm: dialog.querySelector('#btnAddTerm'),
        btnExportTerms: dialog.querySelector('#btnExportTerms'),
        btnImportTerms: dialog.querySelector('#btnImportTerms'),
        termCount: dialog.querySelector('#termCount'),
        /* === 新增 === */
        btnManageTerms: dialog.querySelector('#btnManageTerms')
    };

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

    /* === 自定义术语按钮事件 === */
    /* === 打开/关闭批量添加覆盖层 === */
    let addPanelVisible = false;
    function toggleAddTermPanel(show = !addPanelVisible) {
        addPanelVisible = show;
        addTermPanel.style.display = show ? 'block' : 'none';
        if (show) batchInput.focus();
    }

    elements.btnAddTerm.addEventListener('click', () => toggleAddTermPanel(true));

    btnCancelAdd.addEventListener('click', () => toggleAddTermPanel(false));

    btnSaveTerms.addEventListener('click', () => {
        const raw = batchInput.value;
        if (!raw) { showToast('❌ 内容为空', false); return; }
        const lines = raw.split(/\n+/);
        let added = 0;
        for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed) continue;
            const match = trimmed.match(/^(.+?)[=|]+(.+)$/);
            if (match) {
                const en = match[1].trim();
                const cn = match[2].trim();
                if (en && cn) {
                    userTerms[en] = cn;
                    termMap.set(en, cn);
                    added++;
                }
            }
        }
        if (added) {
            GM_setValue(USER_TERMS_KEY, userTerms);
            updateTermCount();
            showToast(`✅ 已添加 ${added} 条自定义术语`);
            batchInput.value = '';
            toggleAddTermPanel(false);
        } else {
            showToast('❌ 未检测到有效输入', false);
        }
    });

    elements.btnExportTerms.addEventListener('click', exportUserTerms);
    elements.btnImportTerms.addEventListener('click', importUserTerms);

    /* === 新增:管理术语按钮事件 === */
    let managePanelVisible = false;
    function toggleManagePanel(show = !managePanelVisible) {
        managePanelVisible = show;
        manageTermPanel.style.display = show ? 'block' : 'none';
        if (show) {
            // 同步控件默认值
            manageTermPanel.querySelector('#termSearchInput').value = searchTerm = '';
            const perInput = manageTermPanel.querySelector('#itemsPerPageInput');
            perInput.value = itemsPerPage;
            renderUserTermsList();
            // 绑定事件(仅绑定一次)
            if (!perInput.dataset.bound) {
                const searchInput = manageTermPanel.querySelector('#termSearchInput');
                searchInput.addEventListener('input', () => {
                    searchTerm = searchInput.value.trim().toLowerCase();
                    currentPage = 1;
                    renderUserTermsList();
                });
                perInput.addEventListener('change', () => {
                    const v = parseInt(perInput.value);
                    if (!v || v < 1) { perInput.value = 1; itemsPerPage = 1; }
                    else { itemsPerPage = v; }
                    GM_setValue(ITEMS_PER_PAGE_KEY, itemsPerPage);
                    currentPage = 1;
                    renderUserTermsList();
                });
                perInput.dataset.bound = '1';
            }
        }
    }

    elements.btnManageTerms.addEventListener('click', () => toggleManagePanel(true));
    manageTermPanel.querySelector('#btnCloseManage').addEventListener('click', () => toggleManagePanel(false));

    function renderUserTermsList() {
        const listContainer = manageTermPanel.querySelector('#termsList');
        const pageControls = manageTermPanel.querySelector('#paginationControls');
        const allEntries = Object.entries(userTerms);
        const filtered = allEntries.filter(([en, cn]) => en.toLowerCase().includes(searchTerm) || cn.toLowerCase().includes(searchTerm));

        const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage));
        if (currentPage > totalPages) currentPage = totalPages;

        const startIdx = (currentPage - 1) * itemsPerPage;
        const pageEntries = filtered.slice(startIdx, startIdx + itemsPerPage);

        listContainer.innerHTML = '';
        if (!pageEntries.length) {
            listContainer.textContent = '(无匹配结果)';
        } else {
            for (const [en, cn] of pageEntries) {
                const row = document.createElement('div');
                row.style.display = 'flex';
                row.style.justifyContent = 'space-between';
                row.style.alignItems = 'center';
                row.style.margin = '2px 0';
                const textSpan = document.createElement('span');
                textSpan.style.fontFamily = 'monospace';
                textSpan.style.fontSize = '12px';
                textSpan.textContent = `${en} → ${cn}`;
                const delBtn = document.createElement('button');
                delBtn.textContent = '删除';
                delBtn.style.background = '#f44336';
                delBtn.style.border = 'none';
                delBtn.style.borderRadius = '4px';
                delBtn.style.color = '#fff';
                delBtn.style.cursor = 'pointer';
                delBtn.style.fontSize = '12px';
                delBtn.addEventListener('click', () => {
                    deleteUserTerm(en);
                    renderUserTermsList();
                });
                row.appendChild(textSpan);
                row.appendChild(delBtn);
                listContainer.appendChild(row);
            }
        }

        // 渲染分页控件
        pageControls.innerHTML = '';
        if (totalPages > 1) {
            const prevBtn = document.createElement('button');
            prevBtn.textContent = '上一页';
            prevBtn.disabled = currentPage === 1;
            prevBtn.style.marginRight = '8px';
            prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderUserTermsList(); } });

            const nextBtn = document.createElement('button');
            nextBtn.textContent = '下一页';
            nextBtn.disabled = currentPage === totalPages;
            nextBtn.style.marginLeft = '8px';
            nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderUserTermsList(); } });

            const infoSpan = document.createElement('span');
            infoSpan.textContent = `第 ${currentPage} / ${totalPages} 页`;
            pageControls.appendChild(prevBtn);
            pageControls.appendChild(infoSpan);
            pageControls.appendChild(nextBtn);
        }
    }

    /* === 新增:删除自定义术语 === */
    function deleteUserTerm(en) {
        if (!userTerms[en]) return;
        if (!confirm(`确定删除术语:${en} ?`)) return;
        delete userTerms[en];
        GM_setValue(USER_TERMS_KEY, userTerms);
        termMap.delete(en);
        updateTermCount();
        showToast(`✅ 已删除术语:${en}`);
    }

    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');
        // 保存当前位置
        GM_setValue(DIALOG_POS_KEY, { x: dialog.offsetLeft, y: dialog.offsetTop });
    }


    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();
            showToast(`✅ 缓存已清除并重新加载成功,已加载 ${termMap.size} 条术语`);
        } catch (error) {
            console.error('缓存清除失败:', error);
            showToast(`❌ 缓存清除失败:${error.message}`, false);
            termMap.clear();
            updateTermCount();
        }
    }

    async function initTerminology() {
        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));
            }

            /* === 合并并加载用户自定义术语 === */
            userTerms = GM_getValue(USER_TERMS_KEY, {});
            if (userTerms && typeof userTerms === 'object') {
                for (const [en, cn] of Object.entries(userTerms)) {
                    termMap.set(en, cn);
                }
            }
        } 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 lowerMap = new Map(rules.map(([k, v]) => [k.toLowerCase(), v]));
        const snapshot = [];
        const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE', 'TEXTAREA', 'NOSCRIPT']);

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

        while (walker.nextNode()) {
            const node = walker.currentNode;
            if (SKIP_TAGS.has(node.parentNode && node.parentNode.nodeName)) continue;
            const original = node.nodeValue;
            const replaced = original.replace(regex, (m) => lowerMap.get(m.toLowerCase()) ?? m);

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

        if (snapshot.length) {
            replacementHistory.push(snapshot);
            if (replacementHistory.length > HISTORY_LIMIT) {
                replacementHistory.shift();
            }
            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;
        }
    }

    /* ===== 自定义术语相关 ===== */
    function addUserTerm(en, cn) {
        if (!en || !cn) return;
        userTerms[en] = cn;
        GM_setValue(USER_TERMS_KEY, userTerms);
        termMap.set(en, cn);
        updateTermCount();
        showToast(`✅ 已添加术语:${en} → ${cn}`);
    }

    function exportUserTerms() {
        const blob = new Blob([JSON.stringify(userTerms, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'destiny2_custom_terms.json';
        a.click();
        URL.revokeObjectURL(url);
    }

    function importUserTerms() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'application/json';
        input.onchange = () => {
            if (!input.files.length) return;
            const file = input.files[0];
            const reader = new FileReader();
            reader.onload = () => {
                try {
                    const data = JSON.parse(reader.result);
                    if (data && typeof data === 'object') {
                        Object.entries(data).forEach(([en, cn]) => {
                            userTerms[en] = cn;
                            termMap.set(en, cn);
                        });
                        GM_setValue(USER_TERMS_KEY, userTerms);
                        updateTermCount();
                        showToast(`✅ 已导入 ${Object.keys(data).length} 条自定义术语`);
                    } else {
                        showToast('❌ JSON 格式不正确', false);
                    }
                } catch (e) {
                    showToast('❌ 解析失败:' + e.message, false);
                }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    /* ===== Toast 提示 ===== */
    function showToast(message, success = true) {
        const toast = document.createElement('div');
        toast.className = 'gm-toast';
        toast.textContent = message;
        toast.style.background = success ? 'rgba(76,175,80,0.9)' : 'rgba(244,67,54,0.9)';
        document.body.appendChild(toast);
        // 强制触发回流,启用过渡
        void toast.offsetWidth;
        toast.style.opacity = '1';
        setTimeout(() => {
            toast.style.opacity = '0';
            toast.addEventListener('transitionend', () => toast.remove());
        }, 3000);
    }

    /* === 首次使用欢迎提示 === */
    const WELCOME_KEY = 'hasShownWelcome_v2';
    if (!GM_getValue(WELCOME_KEY)) {
        // 延迟以免与面板动画冲突
        setTimeout(()=>showToast('提示:按 Ctrl+Alt+K 或 点击右上角按钮 或 鼠标右键菜单 打开命运2术语替换面板'),500);
        GM_setValue(WELCOME_KEY,true);
    }
})();