您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
替换网页中出现的命运2术语
// ==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); } })();