// ==UserScript==
// @name Bungie.net术语替换_可扩展到其他网站
// @namespace your-namespace
// @version 1.0.1
// @description 在网页中替换文本,支持三种显示模式和术语表替换
// @match https://www.bungie.net/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @connect 20xiji.github.io
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/item-list-250126_simplee.json';
let replacementHistory = [];
let termMap = new Map();
let currentMode = 1;
let dialogVisible = false;
GM_addStyle(`
#textReplacerDialog {
position: fixed;
top: 20px;
right: 20px;
background: #1a1a1a;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
z-index: 9999;
width: 260px;
font-family: Arial, sans-serif;
color: #fff;
display: none;
}
#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;
}
`);
const dialog = document.createElement('div');
dialog.id = 'textReplacerDialog';
dialog.innerHTML = `
<h3 style="margin:0 0 10px 0;font-size:16px;">文本替换工具 <span id="termCount">(加载中...)</span></h3>
<div id="modeButtons">
<button class="mode-btn" data-mode="1">中文模式</button>
<button class="mode-btn" data-mode="2">英文|中文</button>
<button class="mode-btn" data-mode="3">中文(英文)</button>
</div>
<div id="actionButtons">
<button id="btnApplyAll">应用规则</button>
<button id="btnUndo" disabled>撤销</button>
<button id="btnClearCache">清除缓存</button>
</div>
`;
document.body.appendChild(dialog);
const elements = {
modeButtons: dialog.querySelectorAll('.mode-btn'),
btnApplyAll: dialog.querySelector('#btnApplyAll'),
btnUndo: dialog.querySelector('#btnUndo'),
btnClearCache: dialog.querySelector('#btnClearCache'),
termCount: dialog.querySelector('#termCount')
};
elements.modeButtons.forEach(btn => btn.addEventListener('click', handleModeChange));
elements.btnApplyAll.addEventListener('click', applyAllRules);
elements.btnUndo.addEventListener('click', undoReplace);
elements.btnClearCache.addEventListener('click', clearCache);
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'k') {
dialogVisible = !dialogVisible;
dialog.style.display = dialogVisible ? 'block' : 'none';
updateButtonStates();
}
});
initTerminology();
updateButtonStates();
async function clearCache() {
try {
// 强制删除现有缓存
GM_deleteValue('cachedTerms');
GM_deleteValue('cacheTime');
// 立即触发重新加载
const freshData = await fetchTerms();
termMap = new Map(Object.entries(freshData));
GM_setValue('cachedTerms', freshData);
GM_setValue('cacheTime', Date.now());
updateTermCount();
alert('✅ 缓存已清除并重新加载成功\n当前种类:武器、护甲、技能、模组\n已加载条目数:' + termMap.size);
} catch (error) {
console.error('缓存清除失败:', error);
alert('❌ 缓存清除失败:' + error.message);
termMap.clear();
updateTermCount();
}
}
async function initTerminology() {
const CACHE_DAYS = 1;
const cachedData = GM_getValue('cachedTerms');
const cacheTime = GM_getValue('cacheTime', 0);
try {
// 缓存过期或不存在时强制更新
if (!cachedData || Date.now() - cacheTime > 86400000 * CACHE_DAYS) {
const freshData = await fetchTerms();
termMap = new Map(Object.entries(freshData));
GM_setValue('cachedTerms', freshData);
GM_setValue('cacheTime', Date.now());
} else {
termMap = new Map(Object.entries(cachedData));
}
} catch (error) {
console.error('术语表初始化失败:', error);
if (cachedData) {
termMap = new Map(Object.entries(cachedData));
}
}
updateTermCount();
}
function updateTermCount() {
elements.termCount.textContent = termMap.size > 0
? `(已加载${termMap.size}条)`
: '(未加载数据)';
}
function fetchTerms() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: ITEM_LIST_URL,
timeout: 15000,
onload: (res) => {
if (res.status >= 200 && res.status < 300) {
try {
const data = JSON.parse(res.responseText);
if (Object.keys(data).length > 0) {
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秒)'));
}
});
});
}
// 其他保持不变的功能函数
function handleModeChange(e) {
currentMode = parseInt(e.target.dataset.mode);
updateButtonStates();
}
function updateButtonStates() {
elements.modeButtons.forEach(btn => {
btn.classList.toggle('active', parseInt(btn.dataset.mode) === currentMode);
});
}
function applyAllRules() {
const termRules = Array.from(termMap).map(([en, cn]) => {
switch (currentMode) {
case 1: return [en, cn];
case 2: return [en, `${en} | ${cn}`];
case 3: return [en, `${cn}(${en})`];
default: return [en, cn];
}
});
performReplace(termRules);
}
function performReplace(rules) {
const regex = buildRegex(rules);
const replaceMap = new Map(rules);
const snapshot = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
while (walker.nextNode()) {
const node = walker.currentNode;
const original = node.nodeValue;
const replaced = original.replace(regex, (m) => {
const foundKey = Array.from(replaceMap.keys()).find(k =>
k.toLowerCase() === m.toLowerCase()
);
return foundKey ? replaceMap.get(foundKey) : m;
});
if (replaced !== original) {
snapshot.push({ node, text: original });
node.nodeValue = replaced;
}
}
if (snapshot.length) {
replacementHistory.push(snapshot);
elements.btnUndo.disabled = false;
}
}
function buildRegex(rules) {
const sortedKeys = [...new Set(rules.map(([k]) => k))]
.sort((a, b) => b.length - a.length)
.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
return new RegExp(`\\b(${sortedKeys.join('|')})\\b`, 'gi');
}
function undoReplace() {
if (replacementHistory.length) {
const last = replacementHistory.pop();
last.forEach(({ node, text }) => {
if (node.parentNode) node.nodeValue = text;
});
elements.btnUndo.disabled = !replacementHistory.length;
}
}
})();