Destiny2_Term_replace

替换网页中出现的命运2术语 - 支持三语言切换 (EN/简中/繁中)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Destiny2_Term_replace
// @namespace    your-namespace
// @version      5.0
// @description  替换网页中出现的命运2术语 - 支持三语言切换 (EN/简中/繁中)
// @match        *://*/*
// @exclude      *://*.light.gg/*
// @exclude      *://light.gg/*
// @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 USER_TERMS_KEY = 'userDefinedTerms';
    const ITEMS_PER_PAGE_KEY = 'itemsPerPageSetting';
    const LANG_SOURCE_KEY = 'langSource';
    const LANG_TARGET_KEY = 'langTarget';
    const PAUSED_KEY = 'd2tr_paused';
    const WELCOME_KEY = 'hasShownWelcome_v5_panel';
    const PANEL_OPEN_KEY = 'd2tr_panel_open';

    const LANGUAGES = {
        'en': 'English',
        'zh-chs': '简体中文',
        'zh-cht': '繁體中文'
    };

    const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/term-map.json';

    /* === 状态变量 === */
    let replacementHistory = [];
    let termMap = new Map();
    let userTerms = {};
    let currentMode = 1;
    let langSource = GM_getValue(LANG_SOURCE_KEY, 'en');
    let langTarget = GM_getValue(LANG_TARGET_KEY, 'zh-chs');
    let isPaused = GM_getValue(PAUSED_KEY, false);
    let panelOpen = false;
    let currentPage = 1;
    let itemsPerPage = GM_getValue(ITEMS_PER_PAGE_KEY, 20);
    let searchTerm = '';
    let processedNodes = new WeakSet();
    let dataStatus = 'loading'; // loading | ready | error

    function throttle(func, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }

    function clearElement(el) {
        while (el.firstChild) el.removeChild(el.firstChild);
    }

    function createSectionHeader(title) {
        const header = document.createElement('div');
        header.className = 'd2tr-section-header';

        const titleSpan = document.createElement('span');
        titleSpan.className = 'd2tr-section-title';
        titleSpan.textContent = title;

        const arrowSpan = document.createElement('span');
        arrowSpan.className = 'd2tr-section-arrow';
        arrowSpan.textContent = '▼';

        header.appendChild(titleSpan);
        header.appendChild(arrowSpan);
        return header;
    }

    /* === 变体生成 === */
    function addVariants(map, source, target) {
        map.set(source, target);
        if (langSource === 'en' && source.includes("'")) {
            map.set(source.replace(/'/g, '’'), target);
        }
        if (langSource === 'en' && source.startsWith('The ')) {
            map.set(source.slice(4), target);
        }
    }

    function buildTermMapFromData(termMapData, sourceLang, targetLang) {
        const map = new Map();
        const buckets = termMapData.data || termMapData;
        for (const bucketName of Object.keys(buckets)) {
            const bucket = buckets[bucketName];
            for (const hashKey of Object.keys(bucket)) {
                const entry = bucket[hashKey];
                const sourceText = entry[sourceLang];
                const targetText = entry[targetLang];
                if (sourceText && targetText && sourceText !== targetText) {
                    addVariants(map, sourceText, targetText);
                }
            }
        }
        return map;
    }

    /* === 样式 === */
    GM_addStyle(`
        :root {
            --destiny-bg: #0a0e14;
            --destiny-panel: #1a1f2e;
            --destiny-accent: #f0b232;
            --destiny-tech: #00d4ff;
            --destiny-success: #4caf50;
            --destiny-danger: #e74c3c;
            --destiny-text: #ffffff;
            --destiny-text-muted: #a0a8b8;
            --destiny-border: rgba(255, 255, 255, 0.1);
            --destiny-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
            --destiny-radius: 12px;
            --destiny-radius-sm: 8px;
        }

        @keyframes d2tr-fadein {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        @keyframes d2tr-fadeout {
            from { opacity: 1; transform: translateY(0); }
            to { opacity: 0; transform: translateY(10px); }
        }
        @keyframes d2tr-pulse {
            0%, 100% { box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 0 rgba(240, 178, 50, 0.5); }
            50% { box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 10px rgba(240, 178, 50, 0); }
        }

        /* 悬浮按钮 */
        .d2tr-fab {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 48px;
            height: 48px;
            border-radius: 50%;
            border: none;
            background: linear-gradient(135deg, var(--destiny-accent), #e6a020);
            color: var(--destiny-bg);
            font-size: 14px;
            font-weight: 800;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9998;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            user-select: none;
        }
        .d2tr-fab:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 20px rgba(240, 178, 50, 0.4);
        }
        .d2tr-fab.paused {
            background: rgba(255, 255, 255, 0.15);
            color: var(--destiny-text-muted);
        }
        .d2tr-fab.paused:hover {
            background: rgba(255, 255, 255, 0.25);
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
        }
        .d2tr-fab.pulse {
            animation: d2tr-pulse 1.5s ease-in-out 4;
        }

        /* 状态指示灯 */
        .d2tr-status-dot {
            position: absolute;
            top: -2px;
            right: -2px;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            border: 2px solid var(--destiny-panel);
            transition: background 0.3s ease;
        }
        .d2tr-status-dot.loading { background: var(--destiny-accent); }
        .d2tr-status-dot.ready { background: var(--destiny-success); }
        .d2tr-status-dot.error { background: var(--destiny-danger); }

        /* 面板 */
        .d2tr-panel {
            position: fixed;
            bottom: 76px;
            right: 20px;
            width: 300px;
            background: var(--destiny-panel);
            border-radius: var(--destiny-radius);
            box-shadow: var(--destiny-shadow);
            border: 1px solid var(--destiny-border);
            z-index: 9999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            color: var(--destiny-text);
            overflow: hidden;
            backdrop-filter: blur(10px);
            display: none;
        }
        .d2tr-panel.open {
            display: block;
            animation: d2tr-fadein 0.25s ease-out;
        }
        .d2tr-panel.closing {
            animation: d2tr-fadeout 0.2s ease-in forwards;
        }

        /* 面板头部 */
        .d2tr-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 14px 16px;
            border-bottom: 1px solid var(--destiny-border);
            cursor: grab;
        }
        .d2tr-header:active { cursor: grabbing; }
        .d2tr-header-title {
            font-weight: 600;
            font-size: 14px;
            background: linear-gradient(135deg, var(--destiny-accent), var(--destiny-tech));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }
        .d2tr-header-meta {
            font-size: 11px;
            color: var(--destiny-tech);
            padding: 2px 8px;
            background: rgba(0, 212, 255, 0.1);
            border-radius: 10px;
        }

        /* 分组 */
        .d2tr-section {
            border-bottom: 1px solid var(--destiny-border);
        }
        .d2tr-section:last-child { border-bottom: none; }
        .d2tr-section-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 16px;
            cursor: pointer;
            transition: background 0.2s ease;
            user-select: none;
        }
        .d2tr-section-header:hover {
            background: rgba(255, 255, 255, 0.03);
        }
        .d2tr-section-title {
            font-size: 12px;
            font-weight: 600;
            color: var(--destiny-text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .d2tr-section-arrow {
            font-size: 10px;
            color: var(--destiny-text-muted);
            transition: transform 0.2s ease;
        }
        .d2tr-section.open .d2tr-section-arrow {
            transform: rotate(180deg);
        }
        .d2tr-section-body {
            padding: 0 16px 12px;
            display: none;
        }
        .d2tr-section.open .d2tr-section-body {
            display: block;
        }

        /* 语言选择器 */
        .d2tr-lang-row {
            display: flex;
            align-items: center;
            gap: 6px;
            margin-bottom: 8px;
        }
        .d2tr-lang-row label {
            font-size: 11px;
            color: var(--destiny-text-muted);
            white-space: nowrap;
            min-width: 24px;
        }
        .d2tr-lang-row select {
            flex: 1;
            background: rgba(0, 0, 0, 0.3);
            color: var(--destiny-text);
            border: 1px solid var(--destiny-border);
            border-radius: 6px;
            padding: 6px 8px;
            font-size: 12px;
            cursor: pointer;
            transition: border-color 0.2s ease;
        }
        .d2tr-lang-row select:focus {
            outline: none;
            border-color: var(--destiny-accent);
        }
        .d2tr-swap-btn {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid var(--destiny-border);
            color: var(--destiny-accent);
            width: 26px;
            height: 26px;
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 13px;
            transition: all 0.2s ease;
            padding: 0;
            flex-shrink: 0;
        }
        .d2tr-swap-btn:hover {
            background: rgba(240, 178, 50, 0.2);
            transform: rotate(180deg);
        }

        /* 模式按钮 */
        .d2tr-mode-row {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 6px;
        }
        .d2tr-mode-btn {
            padding: 8px 4px;
            border: 1px solid var(--destiny-border);
            border-radius: var(--destiny-radius-sm);
            background: rgba(255, 255, 255, 0.05);
            color: var(--destiny-text-muted);
            cursor: pointer;
            transition: all 0.2s ease;
            font-size: 11px;
            font-weight: 500;
            text-align: center;
        }
        .d2tr-mode-btn:hover {
            background: rgba(255, 255, 255, 0.1);
            color: var(--destiny-text);
            border-color: var(--destiny-accent);
        }
        .d2tr-mode-btn.active {
            background: linear-gradient(135deg, var(--destiny-accent), #e6a020);
            color: var(--destiny-bg);
            border-color: var(--destiny-accent);
            font-weight: 600;
        }

        /* 操作按钮 */
        .d2tr-action-row {
            display: flex;
            gap: 6px;
            margin-top: 8px;
        }
        .d2tr-action-btn {
            flex: 1;
            padding: 8px 6px;
            border: none;
            border-radius: var(--destiny-radius-sm);
            background: linear-gradient(135deg, var(--destiny-accent), #e6a020);
            color: var(--destiny-bg);
            cursor: pointer;
            font-weight: 600;
            font-size: 11px;
            transition: all 0.2s ease;
        }
        .d2tr-action-btn:hover {
            transform: translateY(-1px);
            box-shadow: 0 3px 8px rgba(240, 178, 50, 0.3);
        }
        .d2tr-action-btn:disabled {
            background: rgba(255, 255, 255, 0.1);
            color: var(--destiny-text-muted);
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        .d2tr-action-btn.danger {
            background: linear-gradient(135deg, var(--destiny-danger), #c0392b);
        }
        .d2tr-action-btn.danger:hover {
            box-shadow: 0 3px 8px rgba(231, 76, 60, 0.3);
        }

        /* 术语管理 */
        .d2tr-term-search {
            width: 100%;
            background: rgba(0, 0, 0, 0.3);
            border: 1px solid var(--destiny-border);
            color: var(--destiny-text);
            border-radius: var(--destiny-radius-sm);
            padding: 7px 10px;
            font-size: 12px;
            box-sizing: border-box;
            transition: border-color 0.2s ease;
            margin-bottom: 8px;
        }
        .d2tr-term-search:focus {
            outline: none;
            border-color: var(--destiny-accent);
        }
        .d2tr-term-list {
            max-height: 160px;
            overflow-y: auto;
            background: rgba(0, 0, 0, 0.2);
            border: 1px solid var(--destiny-border);
            border-radius: var(--destiny-radius-sm);
            padding: 4px;
        }
        .d2tr-term-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 8px;
            margin: 2px 0;
            background: rgba(255, 255, 255, 0.02);
            border-radius: 4px;
            transition: background 0.15s ease;
        }
        .d2tr-term-item:hover {
            background: rgba(255, 255, 255, 0.06);
        }
        .d2tr-term-text {
            font-family: 'JetBrains Mono', 'Fira Code', monospace;
            font-size: 11px;
        }
        .d2tr-term-del {
            background: transparent;
            border: 1px solid transparent;
            border-radius: 4px;
            color: var(--destiny-text-muted);
            cursor: pointer;
            font-size: 13px;
            width: 22px;
            height: 22px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.15s ease;
            padding: 0;
        }
        .d2tr-term-del:hover {
            border-color: var(--destiny-danger);
            color: var(--destiny-danger);
            background: rgba(231, 76, 60, 0.1);
        }
        .d2tr-pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 8px;
            margin-top: 8px;
            font-size: 11px;
            color: var(--destiny-text-muted);
        }
        .d2tr-pagination button {
            background: rgba(255, 255, 255, 0.1);
            border: 1px solid var(--destiny-border);
            color: var(--destiny-text-muted);
            border-radius: 4px;
            padding: 4px 10px;
            cursor: pointer;
            font-size: 11px;
            transition: all 0.15s ease;
        }
        .d2tr-pagination button:hover:not(:disabled) {
            background: rgba(255, 255, 255, 0.15);
            color: var(--destiny-text);
        }
        .d2tr-pagination button:disabled {
            opacity: 0.4;
            cursor: not-allowed;
        }

        /* 批量添加面板 */
        .d2tr-batch-area {
            width: 100%;
            height: 80px;
            background: rgba(0, 0, 0, 0.3);
            color: var(--destiny-text);
            border: 1px solid var(--destiny-border);
            border-radius: var(--destiny-radius-sm);
            padding: 8px;
            resize: vertical;
            box-sizing: border-box;
            font-family: 'JetBrains Mono', 'Fira Code', monospace;
            font-size: 11px;
            transition: border-color 0.2s ease;
            margin-top: 6px;
        }
        .d2tr-batch-area:focus {
            outline: none;
            border-color: var(--destiny-accent);
        }
        .d2tr-batch-actions {
            display: flex;
            gap: 6px;
            justify-content: flex-end;
            margin-top: 6px;
        }
        .d2tr-batch-actions button {
            padding: 6px 14px;
            border: none;
            border-radius: var(--destiny-radius-sm);
            cursor: pointer;
            font-weight: 600;
            font-size: 11px;
            transition: all 0.15s ease;
        }
        .d2tr-btn-save {
            background: linear-gradient(135deg, var(--destiny-success), #45a049);
            color: white;
        }
        .d2tr-btn-save:hover {
            transform: translateY(-1px);
            box-shadow: 0 3px 8px rgba(76, 175, 80, 0.3);
        }
        .d2tr-btn-cancel {
            background: rgba(255, 255, 255, 0.1);
            color: var(--destiny-text-muted);
        }
        .d2tr-btn-cancel:hover {
            background: rgba(255, 255, 255, 0.15);
            color: var(--destiny-text);
        }

        /* 高级操作行 */
        .d2tr-adv-row {
            display: flex;
            gap: 6px;
            flex-wrap: wrap;
        }
        .d2tr-adv-btn {
            flex: 1;
            min-width: 60px;
            padding: 7px 6px;
            border: none;
            border-radius: var(--destiny-radius-sm);
            background: rgba(255, 255, 255, 0.08);
            color: var(--destiny-text-muted);
            cursor: pointer;
            font-size: 11px;
            font-weight: 500;
            transition: all 0.15s ease;
            text-align: center;
        }
        .d2tr-adv-btn:hover {
            background: rgba(255, 255, 255, 0.14);
            color: var(--destiny-text);
        }
        .d2tr-adv-btn.danger-text {
            color: var(--destiny-danger);
        }
        .d2tr-adv-btn.danger-text:hover {
            background: rgba(231, 76, 60, 0.15);
        }

        /* Toast */
        .d2tr-toast {
            position: fixed;
            bottom: 76px;
            right: 80px;
            background: var(--destiny-panel);
            color: var(--destiny-text);
            padding: 10px 16px;
            border-radius: var(--destiny-radius-sm);
            font-size: 13px;
            z-index: 10002;
            opacity: 0;
            transition: all 0.3s ease;
            pointer-events: none;
            box-shadow: var(--destiny-shadow);
            border: 1px solid var(--destiny-border);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            align-items: center;
            gap: 6px;
            max-width: 280px;
        }
        .d2tr-toast.show { opacity: 1; }
        .d2tr-toast.success { border-left: 3px solid var(--destiny-success); }
        .d2tr-toast.error { border-left: 3px solid var(--destiny-danger); }
    `);

    /* === Toast 提示 === */
    function showToast(message, success = true) {
        const toast = document.createElement('div');
        toast.className = `d2tr-toast ${success ? 'success' : 'error'}`;
        toast.textContent = message;
        document.body.appendChild(toast);
        requestAnimationFrame(() => toast.classList.add('show'));
        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => toast.remove(), 300);
        }, 2500);
    }

    /* === 创建悬浮按钮 === */
    const fab = document.createElement('button');
    fab.className = 'd2tr-fab' + (isPaused ? ' paused' : '');
    fab.textContent = 'D2';
    fab.title = '命运2术语替换';

    const statusDot = document.createElement('div');
    statusDot.className = 'd2tr-status-dot loading';
    fab.appendChild(statusDot);

    document.body.appendChild(fab);

    /* === 创建面板 === */
    const panel = document.createElement('div');
    panel.className = 'd2tr-panel';

    // 头部
    const header = document.createElement('div');
    header.className = 'd2tr-header';
    const headerTitle = document.createElement('span');
    headerTitle.className = 'd2tr-header-title';
    headerTitle.textContent = '命运2术语替换';
    const headerMeta = document.createElement('span');
    headerMeta.className = 'd2tr-header-meta';
    headerMeta.id = 'd2trTermCount';
    headerMeta.textContent = '加载中...';
    header.appendChild(headerTitle);
    header.appendChild(headerMeta);
    panel.appendChild(header);

    /* --- Section: 语言设置 --- */
    function createLangSection() {
        const section = document.createElement('div');
        section.className = 'd2tr-section open';

        const secHeader = createSectionHeader('语言设置');
        secHeader.addEventListener('click', () => section.classList.toggle('open'));
        section.appendChild(secHeader);

        const body = document.createElement('div');
        body.className = 'd2tr-section-body';

        // 源语言行
        const srcRow = document.createElement('div');
        srcRow.className = 'd2tr-lang-row';
        const srcLabel = document.createElement('label');
        srcLabel.textContent = '从';
        const srcSelect = document.createElement('select');
        srcSelect.id = 'd2trLangSource';
        for (const [code, name] of Object.entries(LANGUAGES)) {
            const opt = document.createElement('option');
            opt.value = code;
            opt.textContent = name;
            if (code === langSource) opt.selected = true;
            srcSelect.appendChild(opt);
        }
        srcRow.appendChild(srcLabel);
        srcRow.appendChild(srcSelect);
        body.appendChild(srcRow);

        // 目标语言行 + 交换按钮
        const tgtRow = document.createElement('div');
        tgtRow.className = 'd2tr-lang-row';
        const tgtLabel = document.createElement('label');
        tgtLabel.textContent = '到';
        const swapBtn = document.createElement('button');
        swapBtn.className = 'd2tr-swap-btn';
        swapBtn.textContent = '⇄';
        swapBtn.title = '交换源语言和目标语言';
        const tgtSelect = document.createElement('select');
        tgtSelect.id = 'd2trLangTarget';
        for (const [code, name] of Object.entries(LANGUAGES)) {
            const opt = document.createElement('option');
            opt.value = code;
            opt.textContent = name;
            if (code === langTarget) opt.selected = true;
            tgtSelect.appendChild(opt);
        }
        tgtRow.appendChild(tgtLabel);
        tgtRow.appendChild(swapBtn);
        tgtRow.appendChild(tgtSelect);
        body.appendChild(tgtRow);

        // 事件
        srcSelect.addEventListener('change', async () => {
            langSource = srcSelect.value;
            GM_setValue(LANG_SOURCE_KEY, langSource);
            await reloadTermMap();
            updateModeButtonsText();
            showToast(`源语言已切换为 ${LANGUAGES[langSource]}`);
        });
        tgtSelect.addEventListener('change', async () => {
            langTarget = tgtSelect.value;
            GM_setValue(LANG_TARGET_KEY, langTarget);
            await reloadTermMap();
            updateModeButtonsText();
            showToast(`目标语言已切换为 ${LANGUAGES[langTarget]}`);
        });
        swapBtn.addEventListener('click', async () => {
            const tmp = srcSelect.value;
            srcSelect.value = tgtSelect.value;
            tgtSelect.value = tmp;
            langSource = srcSelect.value;
            langTarget = tgtSelect.value;
            GM_setValue(LANG_SOURCE_KEY, langSource);
            GM_setValue(LANG_TARGET_KEY, langTarget);
            await reloadTermMap();
            updateModeButtonsText();
            showToast('已交换源语言和目标语言');
        });

        section.appendChild(body);
        return section;
    }

    /* --- Section: 替换模式 --- */
    function createModeSection() {
        const section = document.createElement('div');
        section.className = 'd2tr-section open';

        const secHeader = createSectionHeader('替换模式');
        secHeader.addEventListener('click', () => section.classList.toggle('open'));
        section.appendChild(secHeader);

        const body = document.createElement('div');
        body.className = 'd2tr-section-body';

        const modeRow = document.createElement('div');
        modeRow.className = 'd2tr-mode-row';
        const modes = [1, 2, 3].map(m => {
            const btn = document.createElement('button');
            btn.className = 'd2tr-mode-btn' + (m === currentMode ? ' active' : '');
            btn.dataset.mode = m;
            btn.addEventListener('click', () => {
                currentMode = m;
                modeRow.querySelectorAll('.d2tr-mode-btn').forEach(b => b.classList.toggle('active', parseInt(b.dataset.mode) === m));
            });
            modeRow.appendChild(btn);
            return btn;
        });
        body.appendChild(modeRow);

        // 操作按钮
        const actionRow = document.createElement('div');
        actionRow.className = 'd2tr-action-row';

        const btnApply = document.createElement('button');
        btnApply.className = 'd2tr-action-btn';
        btnApply.textContent = '应用规则';
        btnApply.addEventListener('click', applyAllRules);

        const btnUndo = document.createElement('button');
        btnUndo.className = 'd2tr-action-btn';
        btnUndo.id = 'd2trBtnUndo';
        btnUndo.textContent = '撤销';
        btnUndo.disabled = true;
        btnUndo.addEventListener('click', undoReplace);

        const btnClear = document.createElement('button');
        btnClear.className = 'd2tr-action-btn danger';
        btnClear.textContent = '更新数据';
        btnClear.addEventListener('click', clearCache);

        actionRow.appendChild(btnApply);
        actionRow.appendChild(btnUndo);
        actionRow.appendChild(btnClear);
        body.appendChild(actionRow);

        section.appendChild(body);

        // 暴露模式按钮引用供外部更新文本
        section._modeButtons = modes;
        return section;
    }

    /* --- Section: 自定义术语 --- */
    function createTermSection() {
        const section = document.createElement('div');
        section.className = 'd2tr-section';

        const secHeader = createSectionHeader('自定义术语');
        secHeader.addEventListener('click', () => section.classList.toggle('open'));
        section.appendChild(secHeader);

        const body = document.createElement('div');
        body.className = 'd2tr-section-body';

        // 按钮行
        const btnRow = document.createElement('div');
        btnRow.style.cssText = 'display:flex;gap:6px;margin-bottom:8px;';
        const btnAdd = document.createElement('button');
        btnAdd.className = 'd2tr-adv-btn';
        btnAdd.textContent = '添加';
        const btnManage = document.createElement('button');
        btnManage.className = 'd2tr-adv-btn';
        btnManage.textContent = '管理';
        const btnExport = document.createElement('button');
        btnExport.className = 'd2tr-adv-btn';
        btnExport.textContent = '导出';
        const btnImport = document.createElement('button');
        btnImport.className = 'd2tr-adv-btn';
        btnImport.textContent = '导入';
        btnRow.appendChild(btnAdd);
        btnRow.appendChild(btnManage);
        btnRow.appendChild(btnExport);
        btnRow.appendChild(btnImport);
        body.appendChild(btnRow);

        // 批量添加面板
        const batchPanel = document.createElement('div');
        batchPanel.style.display = 'none';
        const batchHint = document.createElement('p');
        batchHint.style.cssText = 'font-size:11px;color:var(--destiny-text-muted);margin:0 0 6px;line-height:1.4;';
        batchHint.appendChild(document.createTextNode('每行一条,使用 '));
        const eqCode = document.createElement('code');
        eqCode.style.cssText = 'background:rgba(0,212,255,0.1);padding:1px 4px;border-radius:3px;color:var(--destiny-tech);font-size:11px;';
        eqCode.textContent = '=';
        batchHint.appendChild(eqCode);
        batchHint.appendChild(document.createTextNode(' 或 '));
        const pipeCode = document.createElement('code');
        pipeCode.style.cssText = 'background:rgba(0,212,255,0.1);padding:1px 4px;border-radius:3px;color:var(--destiny-tech);font-size:11px;';
        pipeCode.textContent = '|';
        batchHint.appendChild(pipeCode);
        batchHint.appendChild(document.createTextNode(' 分隔'));
        batchPanel.appendChild(batchHint);
        const batchArea = document.createElement('textarea');
        batchArea.className = 'd2tr-batch-area';
        batchArea.placeholder = '在此粘贴术语映射...';
        batchPanel.appendChild(batchArea);
        const batchActions = document.createElement('div');
        batchActions.className = 'd2tr-batch-actions';
        const btnSave = document.createElement('button');
        btnSave.className = 'd2tr-btn-save';
        btnSave.textContent = '保存';
        const btnCancel = document.createElement('button');
        btnCancel.className = 'd2tr-btn-cancel';
        btnCancel.textContent = '取消';
        batchActions.appendChild(btnSave);
        batchActions.appendChild(btnCancel);
        batchPanel.appendChild(batchActions);
        body.appendChild(batchPanel);

        // 管理面板
        const managePanel = document.createElement('div');
        managePanel.style.display = 'none';
        const searchInput = document.createElement('input');
        searchInput.className = 'd2tr-term-search';
        searchInput.placeholder = '搜索术语...';
        managePanel.appendChild(searchInput);
        const termList = document.createElement('div');
        termList.className = 'd2tr-term-list';
        managePanel.appendChild(termList);
        const pagination = document.createElement('div');
        pagination.className = 'd2tr-pagination';
        managePanel.appendChild(pagination);
        const manageClose = document.createElement('div');
        manageClose.style.cssText = 'text-align:right;margin-top:8px;';
        const btnCloseManage = document.createElement('button');
        btnCloseManage.className = 'd2tr-btn-cancel';
        btnCloseManage.textContent = '关闭';
        manageClose.appendChild(btnCloseManage);
        managePanel.appendChild(manageClose);
        body.appendChild(managePanel);

        // 事件
        let batchVisible = false;
        let manageVisible = false;

        btnAdd.addEventListener('click', () => {
            batchVisible = !batchVisible;
            batchPanel.style.display = batchVisible ? 'block' : 'none';
            if (batchVisible) {
                manageVisible = false;
                managePanel.style.display = 'none';
                batchArea.focus();
            }
        });
        btnCancel.addEventListener('click', () => {
            batchVisible = false;
            batchPanel.style.display = 'none';
        });
        btnSave.addEventListener('click', () => {
            const raw = batchArea.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;
                        addVariants(termMap, en, cn);
                        added++;
                    }
                }
            }
            if (added) {
                GM_setValue(USER_TERMS_KEY, userTerms);
                updateTermCount();
                showToast(`已添加 ${added} 条自定义术语`);
                batchArea.value = '';
                batchVisible = false;
                batchPanel.style.display = 'none';
            } else {
                showToast('未检测到有效输入', false);
            }
        });

        btnManage.addEventListener('click', () => {
            manageVisible = !manageVisible;
            managePanel.style.display = manageVisible ? 'block' : 'none';
            if (manageVisible) {
                batchVisible = false;
                batchPanel.style.display = 'none';
                searchInput.value = searchTerm = '';
                renderUserTermsList(termList, pagination, searchInput);
            }
        });
        btnCloseManage.addEventListener('click', () => {
            manageVisible = false;
            managePanel.style.display = 'none';
        });

        searchInput.addEventListener('input', () => {
            searchTerm = searchInput.value.trim().toLowerCase();
            currentPage = 1;
            renderUserTermsList(termList, pagination, searchInput);
        });

        btnExport.addEventListener('click', exportUserTerms);
        btnImport.addEventListener('click', importUserTerms);

        section.appendChild(body);
        section._termList = termList;
        section._pagination = pagination;
        section._searchInput = searchInput;
        return section;
    }


    /* === 组装面板 === */
    const langSection = createLangSection();
    const modeSection = createModeSection();
    const termSection = createTermSection();

    panel.appendChild(langSection);
    panel.appendChild(modeSection);
    panel.appendChild(termSection);
    document.body.appendChild(panel);

    const modeButtons = modeSection._modeButtons;
    const termListEl = termSection._termList;
    const paginationEl = termSection._pagination;
    const searchInputEl = termSection._searchInput;

    /* === 模式按钮文本更新 === */
    function updateModeButtonsText() {
        const srcName = LANGUAGES[langSource];
        const tgtName = LANGUAGES[langTarget];
        const shortSrc = srcName.substring(0, 2);
        const shortTgt = tgtName.substring(0, 2);
        modeButtons[0].textContent = shortTgt;
        modeButtons[0].title = `将${srcName}术语替换为纯${tgtName}`;
        modeButtons[1].textContent = `${shortSrc}|${shortTgt}`;
        modeButtons[1].title = `替换为 "${srcName} | ${tgtName}" 组合`;
        modeButtons[2].textContent = `${shortTgt}(${shortSrc})`;
        modeButtons[2].title = `替换为 "${tgtName}(${srcName})" 组合`;
    }
    updateModeButtonsText();

    /* === 面板开关联动 === */
    function openPanel() {
        panelOpen = true;
        panel.classList.remove('closing');
        panel.classList.add('open');
        GM_setValue(PANEL_OPEN_KEY, true);
    }

    function closePanel() {
        panelOpen = false;
        panel.classList.add('closing');
        setTimeout(() => {
            panel.classList.remove('open', 'closing');
        }, 200);
        GM_setValue(PANEL_OPEN_KEY, false);
    }

    GM_registerMenuCommand('打开/关闭术语面板', () => {
        if (panelOpen) closePanel();
        else openPanel();
    });

    fab.addEventListener('click', () => {
        if (panelOpen) closePanel();
        else openPanel();
    });

    // 点击外部关闭面板
    document.addEventListener('click', (e) => {
        if (panelOpen && !panel.contains(e.target) && !fab.contains(e.target)) {
            closePanel();
        }
    });

    /* === 快捷键 === */
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'k') {
            e.preventDefault();
            if (panelOpen) closePanel();
            else openPanel();
        }
    });

    /* === 暂停/恢复 === */
    function setPaused(paused) {
        isPaused = paused;
        GM_setValue(PAUSED_KEY, paused);
        fab.classList.toggle('paused', paused);
        if (paused && panelOpen) closePanel();
    }

    GM_registerMenuCommand(isPaused ? '▶ 恢复术语替换' : '⏸ 暂停术语替换', () => {
        setPaused(!isPaused);
        showToast(isPaused ? '术语替换已暂停' : '术语替换已恢复');
    });

    /* === 术语计数更新 === */
    function updateTermCount() {
        const countEl = document.getElementById('d2trTermCount');
        if (countEl) {
            countEl.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) resolve(data);
                            else reject(new Error('获取到空数据'));
                        } catch (e) {
                            reject(new Error('数据解析失败'));
                        }
                    } else {
                        reject(new Error(`HTTP ${res.status}`));
                    }
                },
                onerror: (err) => reject(new Error(`网络错误: ${err}`)),
                ontimeout: () => reject(new Error('请求超时(15秒)'))
            });
        });
    }

    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 = buildTermMapFromData(freshData, langSource, langTarget);
                GM_setValue('cachedTerms', freshData);
                GM_setValue('cacheTime', Date.now());
            } else {
                termMap = buildTermMapFromData(cachedData, langSource, langTarget);
            }
            userTerms = GM_getValue(USER_TERMS_KEY, {});
            if (userTerms && typeof userTerms === 'object') {
                for (const [source, target] of Object.entries(userTerms)) {
                    addVariants(termMap, source, target);
                }
            }
            dataStatus = 'ready';
            statusDot.className = 'd2tr-status-dot ready';
        } catch (error) {
            console.error('术语表初始化失败:', error);
            dataStatus = 'error';
            statusDot.className = 'd2tr-status-dot error';
            if (cachedData) {
                termMap = buildTermMapFromData(cachedData, langSource, langTarget);
                showToast('术语表加载失败,已使用缓存数据', false);
            } else {
                showToast('术语表加载失败且无缓存可用', false);
            }
        }
        updateTermCount();
    }

    async function reloadTermMap() {
        const cachedData = GM_getValue('cachedTerms');
        if (cachedData) {
            termMap = buildTermMapFromData(cachedData, langSource, langTarget);
            userTerms = GM_getValue(USER_TERMS_KEY, {});
            if (userTerms && typeof userTerms === 'object') {
                for (const [source, target] of Object.entries(userTerms)) {
                    addVariants(termMap, source, target);
                }
            }
            updateTermCount();
            processedNodes = new WeakSet();
        }
    }

    async function clearCache() {
        try {
            GM_deleteValue('cachedTerms');
            GM_deleteValue('cacheTime');
            const freshData = await fetchTerms();
            termMap = buildTermMapFromData(freshData, langSource, langTarget);
            GM_setValue('cachedTerms', freshData);
            GM_setValue('cacheTime', Date.now());
            dataStatus = 'ready';
            statusDot.className = 'd2tr-status-dot ready';
            updateTermCount();
            showToast(`缓存已清除,已加载 ${termMap.size} 条术语`);
        } catch (error) {
            console.error('缓存清除失败:', error);
            dataStatus = 'error';
            statusDot.className = 'd2tr-status-dot error';
            showToast(`缓存清除失败:${error.message}`, false);
            termMap.clear();
            updateTermCount();
        }
    }

    /* === 替换逻辑 === */
    function applyAllRules() {
        if (isPaused) { showToast('术语替换已暂停,请先恢复', false); return; }
        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];
            }
        });
        const count = performReplace(termRules);
        if (count > 0) {
            showToast(`已替换 ${count} 个术语`);
        } else {
            showToast('未找到可替换的术语');
        }
    }

    function performReplace(rules) {
        if (!rules.length) return 0;
        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 (processedNodes.has(node)) continue;
            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;
                processedNodes.add(node);
            }
        }

        if (snapshot.length) {
            replacementHistory.push(snapshot);
            if (replacementHistory.length > HISTORY_LIMIT) replacementHistory.shift();
            document.getElementById('d2trBtnUndo').disabled = false;
        }
        return snapshot.length;
    }

    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;
                    processedNodes.delete(node);
                }
            });
            document.getElementById('d2trBtnUndo').disabled = !replacementHistory.length;
            showToast('已撤销上次替换');
        }
    }

    /* === 自定义术语管理 === */
    function renderUserTermsList(listContainer, pageControls, searchEl) {
        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);

        clearElement(listContainer);
        if (!pageEntries.length) {
            const empty = document.createElement('div');
            empty.style.cssText = 'text-align:center;color:var(--destiny-text-muted);padding:16px;font-size:12px;';
            empty.textContent = '无匹配结果';
            listContainer.appendChild(empty);
        } else {
            for (const [en, cn] of pageEntries) {
                const row = document.createElement('div');
                row.className = 'd2tr-term-item';
                const textSpan = document.createElement('span');
                textSpan.className = 'd2tr-term-text';
                const sourceSpan = document.createElement('span');
                sourceSpan.style.color = 'var(--destiny-tech)';
                sourceSpan.textContent = en;
                const arrowSpan = document.createElement('span');
                arrowSpan.style.color = 'var(--destiny-text-muted)';
                arrowSpan.textContent = ' → ';
                const targetSpan = document.createElement('span');
                targetSpan.style.color = 'var(--destiny-accent)';
                targetSpan.textContent = cn;
                textSpan.appendChild(sourceSpan);
                textSpan.appendChild(arrowSpan);
                textSpan.appendChild(targetSpan);
                const delBtn = document.createElement('button');
                delBtn.className = 'd2tr-term-del';
                delBtn.textContent = '×';
                delBtn.addEventListener('click', () => {
                    deleteUserTerm(en);
                    renderUserTermsList(listContainer, pageControls, searchEl);
                });
                row.appendChild(textSpan);
                row.appendChild(delBtn);
                listContainer.appendChild(row);
            }
        }

        clearElement(pageControls);
        if (totalPages > 1) {
            const mkBtn = (txt, dis, fn) => {
                const b = document.createElement('button');
                b.textContent = txt;
                b.disabled = dis;
                b.addEventListener('click', fn);
                return b;
            };
            pageControls.appendChild(mkBtn('上一页', currentPage === 1, () => {
                if (currentPage > 1) { currentPage--; renderUserTermsList(listContainer, pageControls, searchEl); }
            }));
            const info = document.createElement('span');
            info.textContent = `${currentPage} / ${totalPages}`;
            pageControls.appendChild(info);
            pageControls.appendChild(mkBtn('下一页', currentPage === totalPages, () => {
                if (currentPage < totalPages) { currentPage++; renderUserTermsList(listContainer, pageControls, searchEl); }
            }));
        }
    }

    function deleteUserTerm(en) {
        if (!userTerms[en]) return;
        if (!confirm(`确定删除术语:${en} → ${userTerms[en]}?`)) return;
        delete userTerms[en];
        GM_setValue(USER_TERMS_KEY, userTerms);
        reloadTermMap();
        showToast(`已删除术语:${en}`);
    }

    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);
        showToast('已导出自定义术语');
    }

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

    /* === 初始化 === */
    initTerminology().then(() => {
        // 首次安装引导
        if (!GM_getValue(WELCOME_KEY)) {
            fab.classList.add('pulse');
            setTimeout(() => {
                fab.classList.remove('pulse');
            }, 6000);
            setTimeout(() => showToast('点击右下角按钮打开 Destiny 2 术语工具'), 500);
            GM_setValue(WELCOME_KEY, true);
        }

        // 恢复上次面板状态
        if (GM_getValue(PANEL_OPEN_KEY, false)) {
            openPanel();
        }
    });

})();