// ==UserScript==
// @name Web+
// @namespace WebPlus
// @description 综合性网页体验增强脚本。模块化设计,按需启用,精准匹配,性能优先。
// @version 1.0.1
// @license MIT
// @author Qiu Zongman
// @homepageURL https://gitee.com/qiuzongman/WebPlus
// @match *://*/*
// @connect edge.microsoft.com
// @connect api-edge.cognitive.microsofttranslator.com
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-end
// @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTI5OSIgaGVpZ2h0PSIxMzAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBvdmVyZmxvdz0iaGlkZGVuIj48ZGVmcz48Y2xpcFBhdGggaWQ9ImNsaXAwIj48cmVjdCB4PSIzMTAxIiB5PSI0ODIiIHdpZHRoPSIxMjk5IiBoZWlnaHQ9IjEzMDAiLz48L2NsaXBQYXRoPjwvZGVmcz48ZyBjbGlwLXBhdGg9InVybCgjY2xpcDApIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzEwMSAtNDgyKSI+PHJlY3QgeD0iMzEwMSIgeT0iNDgzIiB3aWR0aD0iMTI5OSIgaGVpZ2h0PSIxMjk5IiBmaWxsPSIjRkZGRkZGIi8+PHJlY3QgeD0iMzEwMSIgeT0iMTE5NyIgd2lkdGg9IjU4NCIgaGVpZ2h0PSI1ODUiIGZpbGw9IiMzNEE4NTMiLz48cmVjdCB4PSIzMTAxIiB5PSI0ODIiIHdpZHRoPSI1ODQiIGhlaWdodD0iNTg0IiBmaWxsPSIjRUE0MzM1Ii8+PHJlY3QgeD0iMzgxNSIgeT0iMTE5NyIgd2lkdGg9IjU4NSIgaGVpZ2h0PSI1ODUiIGZpbGw9IiNGQkJDMDQiLz48cmVjdCB4PSIzODE1IiB5PSI3MDkiIHdpZHRoPSI1ODUiIGhlaWdodD0iMTMwIiBmaWxsPSIjNDI4NUY0Ii8+PHJlY3QgeD0iNDA0MyIgeT0iNDg0IiB3aWR0aD0iMTMwIiBoZWlnaHQ9IjU4NSIgZmlsbD0iIzQyODVGNCIvPjwvZz48L3N2Zz4=
// ==/UserScript==
(function () {
'use strict';
if (window.top !== window.self) return;
// ===================================================================
// 核心框架 — 设置管理
// ===================================================================
const STORAGE_KEY = 'wp_settings';
const DEFAULT_SETTINGS = {
lastTab: 4,
pageToggleKeys: {
translate: '',
},
pageEnabled: {
translate: true,
global: true,
other: true,
visual: true,
speed: true,
about: true,
},
// 各模块专属设置
modules: {
translate: {
blockedSites: '',
selTranslate: false,
autoTranslate: false,
btnMode: 'hide',
pageShortcut: 'Alt+A',
selShortcut: 'Alt+S',
bilingualKey: '',
},
bilibili: {
showIp: true,
colorMode: '默认',
cleanLinks: true,
directLink: true,
preloadBoost: true,
hoverDelay: 65,
cleanSources: {},
highlightEnabled: false,
highlightKeywords: '',
highlightColor: '#FF0000',
scrollbarStyle: 'default',
scrollbarColor: '#FF0000',
backToTop: false,
spacingEnabled: false,
unlockEnabled: false,
}
},
// 全局设置
openSettingsKey: '',
};
let settings = {};
function loadSettings() {
try {
const raw = typeof GM_getValue === 'function' ? GM_getValue(STORAGE_KEY, null) : null;
settings = raw ? { ...DEFAULT_SETTINGS, ...JSON.parse(raw) } : { ...DEFAULT_SETTINGS };
if (!settings.pageEnabled) settings.pageEnabled = { ...DEFAULT_SETTINGS.pageEnabled };
if (!settings.modules) settings.modules = { ...DEFAULT_SETTINGS.modules };
// 确保每个模块默认值存在
for (const key in DEFAULT_SETTINGS.modules) {
if (!settings.modules[key]) settings.modules[key] = { ...DEFAULT_SETTINGS.modules[key] };
else for (const dk in DEFAULT_SETTINGS.modules[key]) {
if (settings.modules[key][dk] === undefined) settings.modules[key][dk] = DEFAULT_SETTINGS.modules[key][dk];
}
}
} catch (e) {
settings = { ...DEFAULT_SETTINGS };
}
}
function saveSettings() {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(STORAGE_KEY, JSON.stringify(settings));
}
} catch (e) { console.error('wp saveSettings error:', e); }
}
// ===================================================================
// 核心框架 — 页面定义
// ===================================================================
const PAGES = [
{ id: 'translate', icon: '🌐', label: '网页翻译' },
{ id: 'visual', icon: '🎨', label: '视觉效果' },
{ id: 'speed', icon: '⚡', label: '优化提速' },
{ id: 'other', icon: '🧩', label: '零散工具' },
{ id: 'global', icon: '⚙️', label: '全局设置' },
{ id: 'about', icon: '📋', label: '关于' },
];
// ===================================================================
// 核心框架 — UI 面板
// ===================================================================
function buildPageContent(pageId) {
// 由各模块覆盖扩展
return '
功能待开发
';
}
function openSettings() {
const existing = document.getElementById('wp-settings-panel');
if (existing) { existing.remove(); return; }
const panel = document.createElement('div');
panel.id = 'wp-settings-panel';
panel.innerHTML = buildHTML();
document.body.appendChild(panel);
bindEvents(panel);
// 通知各模块刷新 UI(如翻译快捷键输入框的重置)
document.dispatchEvent(new CustomEvent('wp-panel-open'));
}
function buildHTML() {
const esc = (v) => String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
const navItems = PAGES.map((p, i) => `
`).join('');
const pageContents = PAGES.map(p => {
return `
${buildPageContent(p.id)}
`;
}).join('');
return `
${pageContents}
`;
}
function bindEvents(panel) {
const navBtns = panel.querySelectorAll('.wp-nav-btn');
const pages = panel.querySelectorAll('.wp-page');
const toggles = panel.querySelectorAll('.wp-page-toggle');
function switchPage(index) {
const pageId = PAGES[index].id;
navBtns.forEach((btn, i) => btn.classList.toggle('active', i === index));
pages.forEach(p => { p.style.display = p.dataset.pageId === pageId ? 'flex' : 'none'; });
settings.lastTab = index;
saveSettings();
}
navBtns.forEach((btn, i) => { btn.addEventListener('click', () => switchPage(i)); });
const initIndex = Math.min(settings.lastTab || 0, PAGES.length - 1);
switchPage(initIndex);
// 页面开关 — 即时保存
toggles.forEach(t => {
t.addEventListener('change', function () {
const label = this.nextElementSibling.nextElementSibling;
if (this.checked) {
label.textContent = '已启用';
label.style.color = 'var(--wp-toggle-on, #4CAF50)';
} else {
label.textContent = '已关闭';
label.style.color = 'var(--wp-text-muted, #888)';
}
settings.pageEnabled[this.dataset.page] = this.checked;
saveSettings();
document.dispatchEvent(new CustomEvent('wp-module-toggle', {
detail: { page: this.dataset.page, enabled: this.checked }
}));
});
});
// 监听外部(如快捷键)触发的模块切换,同步 UI
document.addEventListener('wp-module-toggle', function syncToggle(e) {
var cb = panel.querySelector('.wp-page-toggle[data-page="' + e.detail.page + '"]');
if (!cb) return;
cb.checked = e.detail.enabled;
var label = cb.nextElementSibling.nextElementSibling;
if (label) {
label.textContent = e.detail.enabled ? '已启用' : '已关闭';
label.style.color = e.detail.enabled ? 'var(--wp-toggle-on, #4CAF50)' : 'var(--wp-text-muted, #888)';
}
});
// 全局设置 — 网页翻译开关
var globalTrans = panel.querySelector('#wp-global-translate');
if (globalTrans) {
globalTrans.querySelectorAll('.wp-dual-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.pageEnabled.translate = v;
saveSettings();
globalTrans.querySelectorAll('.wp-dual-btn').forEach(function(x) {
x.classList.toggle('active', x.dataset.value === String(v));
});
document.dispatchEvent(new CustomEvent('wp-module-toggle', {
detail: { page: 'translate', enabled: v }
}));
});
});
}
// 关闭
panel.querySelector('#wp-close').addEventListener('click', () => panel.remove());
// 恢复默认(在全局页面底部)
panel.querySelector('#wp-reset-global').addEventListener('click', () => {
settings = { ...DEFAULT_SETTINGS };
saveSettings();
panel.remove();
location.reload();
});
// 快捷键捕获
document.querySelectorAll('.wp-key-input').forEach(function(inp) {
inp.addEventListener('focus', function() { console.log('wp-key focus on', this.id); this.value = ''; });
inp.addEventListener('keydown', function(e) {
console.log('wp-key keydown on', this.id, 'key:', e.key);
if (e.key === 'Escape' || e.key === 'Delete' || e.key === 'Backspace') {
this.value = ''; saveSettings(); this.blur(); e.preventDefault(); return;
}
if (['Control','Alt','Shift','Meta'].indexOf(e.key) >= 0) return;
var k = e.key === ' ' ? 'Space' : e.key;
if (k.length > 1 && !/^F\d+$/.test(k)) return;
if (e.ctrlKey) k = 'Ctrl+' + k;
if (e.altKey) k = 'Alt+' + k;
if (e.shiftKey) k = 'Shift+' + k;
this.value = k; saveSettings(); this.blur(); e.preventDefault();
});
inp.addEventListener('blur', function() {
var val = this.value;
if (this.id === 'wp-openSettingsKey') settings.openSettingsKey = val;
else if (this.id === 'wp-toggle-key-translate') { if (!settings.pageToggleKeys) settings.pageToggleKeys = {}; settings.pageToggleKeys.translate = val; }
else if (this.classList.contains('wp-trans-key')) { settings.modules.translate[this.dataset.key] = val; }
else if (this.dataset.key) {
var p = this.dataset.key.split('.');
var o = settings;
for (var pi = 0; pi < p.length - 1; pi++) { if (!o[p[pi]]) o[p[pi]] = {}; o = o[p[pi]]; }
o[p[p.length - 1]] = val;
}
saveSettings();
});
});
function onEsc(e) { if (e.key === 'Escape') { panel.remove(); document.removeEventListener('keydown', onEsc); } }
document.addEventListener('keydown', onEsc);
// 更新所有外部规则
var updateBtn = document.getElementById('wp-update-rules');
var updateStatus = document.getElementById('wp-update-rules-status');
if (updateBtn && updateStatus) {
updateBtn.addEventListener('click', function() {
updateBtn.disabled = true; updateBtn.textContent = '更新中…';
var srcs = settings.modules.bilibili && settings.modules.bilibili.cleanSources || {};
var urls = Object.keys(srcs);
if (!urls.length) { updateStatus.textContent = '没有已加载的外部规则。'; updateBtn.disabled = false; updateBtn.textContent = '🔄 更新所有外部链接文件'; return; }
var done = 0, total = urls.length;
urls.forEach(function(url) {
try {
BILI_MODULE.loadCleanSource(url, function() { done++; updateStatus.textContent = '更新中 ' + done + '/' + total; if (done >= total) { updateStatus.textContent = '✅ 更新完成'; updateBtn.disabled = false; updateBtn.textContent = '🔄 更新所有外部链接文件'; } });
} catch(e) { done++; if (done >= total) { updateStatus.textContent = '更新完成'; updateBtn.disabled = false; updateBtn.textContent = '🔄 更新所有外部链接文件'; } }
});
});
}
// 通知各模块绑定面板事件
try { TRANS_MODULE.bindPanelEvents(); } catch(e) {}
try { BILI_MODULE.bindPanelEvents(); } catch(e) {}
}
// ===================================================================
// 翻译模块
// ===================================================================
const TRANS_MODULE = (function() {
'use strict';
if (window.top !== window.self) return;
const AUTH = 'https://edge.microsoft.com/translate/auth';
const API = 'https://api-edge.cognitive.microsofttranslator.com/translate?api-version=3.0&to=zh-Hans&textType=plain';
const API_HTML = 'https://api-edge.cognitive.microsofttranslator.com/translate?api-version=3.0&to=zh-Hans&textType=html';
const MAX_UNITS = 5000, MAX_CHARS = 500000, CHUNK_NODES = 25, CHUNK_CHARS = 8000, CONCURRENCY = 6, CACHE_LIMIT = 5000;
let token = '', tokenTime = 0, running = false;
let cache = {}, revCache = {};
let cacheKeys = [];
let isTranslated = false, translateInProgress = false, translateCount = 0;
let showRestore = false;
function mod(){return settings.modules.translate||{}}
function mod(){return settings.modules.translate||{}}
let everTranslated = false;
const ZH_RE = /[\u4e00-\u9fff]/;
const HTML_PREFIX = '__MS_AUTO_ZH_HTML__';
function htmlKey(s) { return HTML_PREFIX + s; }
function stripHtmlKey(s) { return s.startsWith(HTML_PREFIX) ? s.slice(HTML_PREFIX.length) : s; }
function setCache(src, dst) {
if (!src || !dst || cache[src]) return;
dst = dst.trim();
cache[src] = dst;
// 多对一映射:相同译文可对应多份原文,存数组
if (revCache[dst] === undefined) {
revCache[dst] = src;
} else if (Array.isArray(revCache[dst])) {
revCache[dst].push(src);
} else if (revCache[dst] !== src) {
revCache[dst] = [revCache[dst], src];
}
cacheKeys.push(src);
if (cacheKeys.length > CACHE_LIMIT) {
const old = cacheKeys.shift();
const oldDst = cache[old];
if (oldDst) {
if (Array.isArray(revCache[oldDst])) {
revCache[oldDst] = revCache[oldDst].filter(v => v !== old);
if (revCache[oldDst].length === 0) delete revCache[oldDst];
else if (revCache[oldDst].length === 1) revCache[oldDst] = revCache[oldDst][0];
} else {
delete revCache[oldDst];
}
}
delete cache[old];
}
}
// revCache 读值辅助(兼容多对一)
function revGet(dst) {
const v = revCache[dst];
if (v === undefined) return null;
return Array.isArray(v) ? v[0] : v;
}
// ---------- 扫描渲染函数 ----------
const MODE_ORIG = 0, MODE_TRANS = 1, MODE_BI = 2;
let currentMode = MODE_ORIG;
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(//g,'>');
}
function cleanupBiSpans() {
let list = document.querySelectorAll('.ms-bi');
let iter = 0;
while (list.length > 0 && iter < 10) {
for (let i = list.length - 1; i >= 0; i--) {
const w = list[i];
if (!w.parentNode) continue;
const biType = w.dataset.biType || 'text';
const transEl = w.querySelector('.ms-bi-trans');
const transText = transEl ? (biType === 'block' ? transEl.innerHTML : transEl.textContent)
: (w.dataset.trans || '');
if (biType === 'block') {
w.parentNode.innerHTML = transText;
} else {
w.parentNode.replaceChild(document.createTextNode(transText), w);
}
}
list = document.querySelectorAll('.ms-bi');
iter++;
}
}
function renderOriginalFromCache() {
cleanupBiSpans();
const all = document.querySelectorAll('body *');
for (let i = 0; i < all.length; i++) {
const el = all[i];
const cur = el.innerHTML;
var orig = revGet(cur);
if (orig) {
el.innerHTML = stripHtmlKey(orig);
}
}
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let n;
while ((n = walker.nextNode())) {
const v = n.nodeValue;
const trimmed = v.trim();
var origText = revGet(trimmed);
if (origText) {
n.nodeValue = v.split(trimmed).join(origText);
}
}
currentMode = MODE_ORIG;
}
function renderTranslationFromCache() {
cleanupBiSpans();
const all = document.querySelectorAll('body *');
for (let i = 0; i < all.length; i++) {
const el = all[i];
const key = htmlKey(el.innerHTML);
if (cache[key]) {
el.innerHTML = cache[key];
}
}
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let n;
while ((n = walker.nextNode())) {
const v = n.nodeValue;
const trimmed = v.trim();
if (cache[trimmed]) {
n.nodeValue = v.split(trimmed).join(cache[trimmed]);
}
}
currentMode = MODE_TRANS;
}
function renderBilingual() {
cleanupBiSpans();
const processedEls = new WeakSet();
const nodesToReplace = [];
const allEls = document.querySelectorAll('body *');
for (const el of allEls) {
const curHtml = el.innerHTML;
const key = htmlKey(curHtml);
let origHtml, transHtml;
if (cache[key]) {
origHtml = stripHtmlKey(key);
transHtml = cache[key];
} else {
var origFromHtml = revGet(curHtml);
if (origFromHtml) {
transHtml = curHtml;
origHtml = stripHtmlKey(origFromHtml);
} else continue;
}
processedEls.add(el);
nodesToReplace.push({ type: 'block', el, origHtml, transHtml });
}
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let n;
while ((n = walker.nextNode())) {
if (n.parentElement && processedEls.has(n.parentElement)) continue;
const v = n.nodeValue;
const trimmed = v.trim();
let orig, trans;
if (cache[trimmed]) {
orig = trimmed;
trans = cache[trimmed];
} else {
var origTextFromRev = revGet(trimmed);
if (origTextFromRev) {
orig = origTextFromRev;
trans = trimmed;
} else continue;
}
const idx = v.indexOf(trimmed);
if (idx < 0) continue;
const head = v.slice(0, idx);
const tail = v.slice(idx + trimmed.length);
nodesToReplace.push({ type: 'text', node: n, head, orig, trans, tail });
}
for (const item of nodesToReplace) {
if (item.type === 'block') {
const wrapper = document.createElement('span');
wrapper.className = 'ms-bi';
wrapper.dataset.biType = 'block';
wrapper.dataset.trans = item.transHtml;
wrapper.innerHTML = `${item.origHtml} | ${item.transHtml}`;
item.el.innerHTML = '';
item.el.appendChild(wrapper);
} else {
const wrapper = document.createElement('span');
wrapper.className = 'ms-bi';
wrapper.dataset.biType = 'text';
wrapper.dataset.trans = item.trans;
wrapper.innerHTML = `${escHtml(item.orig)} | ${escHtml(item.trans)}`;
const parent = item.node.parentNode;
if (!parent) continue;
parent.insertBefore(document.createTextNode(item.head), item.node);
parent.insertBefore(wrapper, item.node);
parent.insertBefore(document.createTextNode(item.tail), item.node);
parent.removeChild(item.node);
}
}
currentMode = MODE_BI;
}
// ---------- 翻译收集与 API 调用 ----------
const EXT_RE = /\.(apk|zip|7z|rar|tar|gz|iso|json|xml|yaml|txt|md|exe|dll|js|css|html)$/i;
const URL_RE = /^https?:\/\//i;
const TECH_STR_RE = /^[A-Za-z0-9._+@#:/\\()[\]-]+$/;
const HEX_RE = /^[a-f0-9]{7,40}$/i;
const VERSION_RE = /^v?\d+(\.\d+){1,4}([-+][A-Za-z0-9._-]+)?$/i;
const CONST_RE = /^[A-Z0-9_]{2,12}$/;
const CAMEL_RE = /^[A-Za-z]+[A-Z][A-Za-z0-9_.$-]*$/;
const PUNCT_CODE_RE = /[,:;()]/;
const METHOD_RE = /\b[A-Za-z_$][A-Za-z0-9_$]*\s*\(\s*\)/g;
const ID_RE = /\b[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*\b/g;
const COMMON_EN_WORDS = new Set(['the','a','an','is','are','was','were','be','been','have','has','had','do','does','did','will','would','shall','should','may','might','can','could','of','in','on','at','to','for','with','and','or','but','not','this','that','these','those','it','they','we','you','he','she','i','from','by','about','as','into','like','through','after','over','between','out','against','during','without','before','under','around','among']);
function hasNaturalLanguage(s) {
let count = 0;
const words = s.toLowerCase().split(/\s+/);
for (const w of words) if (COMMON_EN_WORDS.has(w) && ++count >= 2) return true;
return false;
}
function req(o, ok, bad) {
const opt = { method: o.method || 'GET', url: o.url, headers: o.headers || {}, data: o.data, timeout: o.timeout || 16000 };
if (typeof GM !== 'undefined' && GM.xmlHttpRequest)
GM.xmlHttpRequest(opt).then(ok).catch(e => bad(e?.message || String(e)));
else if (typeof GM_xmlhttpRequest === 'function')
GM_xmlhttpRequest({ method: opt.method, url: opt.url, headers: opt.headers, data: opt.data, timeout: opt.timeout, onload: ok, onerror: () => bad('请求失败'), ontimeout: () => bad('请求超时') });
else
fetch(opt.url, { method: opt.method, headers: opt.headers, body: opt.data })
.then(r => r.text().then(t => ok({ status: r.status, responseText: t })))
.catch(e => bad(e?.message || String(e)));
}
function cleanup(s) {
s = String(s || '').trim();
s = s.replace(/([\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])/g, '$1');
s = s.replace(/\s+([,。!?:;、)】》])/g, '$1');
s = s.replace(/([(【《])\s+/g, '$1');
s = s.replace(/([\u4e00-\u9fff])\s+([,。!?:;、])/g, '$1$2');
return s;
}
function isCJK(cp) { return cp >= 0x4E00 && cp <= 0x9FFF; }
function isLatin(cp) { return (cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A); }
function isLetter(cp) {
return (cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A) ||
(cp >= 0xC0 && cp <= 0x24F) || (cp >= 0x370 && cp <= 0x3FF) ||
(cp >= 0x400 && cp <= 0x52F) || (cp >= 0x590 && cp <= 0x5FF) ||
(cp >= 0x600 && cp <= 0x6FF) || (cp >= 0x900 && cp <= 0xDFF) ||
(cp >= 0xE00 && cp <= 0xE7F) || (cp >= 0x1000 && cp <= 0x109F) ||
(cp >= 0x10A0 && cp <= 0x10FF) || (cp >= 0x1200 && cp <= 0x137F) ||
(cp >= 0x1780 && cp <= 0x17FF) || (cp >= 0x3040 && cp <= 0x309F) ||
(cp >= 0x30A0 && cp <= 0x30FF) || (cp >= 0xAC00 && cp <= 0xD7AF);
}
function countLetters(t, nonLatinOnly) {
return Array.from(String(t || '')).filter(c => {
const cp = c.charCodeAt(0);
if (isCJK(cp)) return false;
if (nonLatinOnly && isLatin(cp)) return false;
return isLetter(cp);
}).length;
}
function should(t) {
if (!t || t.length < 2) return false;
if (protect(t)) return false;
let zhCount = 0;
for (let i = 0; i < t.length; i++) if (isCJK(t.charCodeAt(i))) zhCount++;
if (zhCount > 0) return countLetters(t, true) >= 1;
return countLetters(t, false) >= 1;
}
function protect(t) {
const s = String(t || '').trim();
if (!s || s.length < 2) return true;
if (EXT_RE.test(s)) return true;
if (URL_RE.test(s)) return true;
if (TECH_STR_RE.test(s) && (s.match(/[._@#:/\\()[\]-]/g) || []).length >= 2) return true;
if (HEX_RE.test(s)) return true;
if (VERSION_RE.test(s)) return true;
if (CONST_RE.test(s) && !/^(ALL|TOP|NEW|YES|NO|OK)$/.test(s)) return true;
if (/^\.[a-zA-Z]/.test(s)) return true;
if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.[a-zA-Z0-9]{2,6}$/.test(s)) return true;
if (CAMEL_RE.test(s)) return true;
const methodCalls = s.match(METHOD_RE) || [];
if (methodCalls.length >= 2) return true;
const ids = s.match(ID_RE) || [];
let codeIds = 0;
for (const id of ids) {
if (/[a-z][A-Z]|[_$]/.test(id) || /^[A-Z][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*$/.test(id)) codeIds++;
}
if (methodCalls.length >= 1 && codeIds >= 1) return true;
if (codeIds >= 3 && PUNCT_CODE_RE.test(s)) return !hasNaturalLanguage(s);
let hasAnyLetter = false;
for (let i = 0; i < s.length; i++) {
const cp = s.charCodeAt(i);
if (isCJK(cp) || isLetter(cp)) { hasAnyLetter = true; break; }
}
if (!hasAnyLetter || /^[A-Z]$/.test(s)) return true;
return false;
}
function rectEl(el) {
if (!el || el.nodeType !== 1) return null;
const r = el.getBoundingClientRect();
if (!r || (r.width === 0 && r.height === 0)) return { top: 1e9, bottom: 0, left: 0, right: 0, width: 0, height: 0 };
return r;
}
function simpleRichBlock(el) {
if (!el || !el.querySelector) return false;
if (el.querySelector('pre,code,kbd,samp,var,script,style,textarea,input,select,button,svg,canvas,math,table')) return false;
const forbidden = el.querySelectorAll('*:not(a,span,b,strong,i,em,u,mark,small,sub,sup,br,ul,ol,li,nav,aside,header,footer)');
return forbidden.length === 0;
}
function collectShadowRoots(root) {
const roots = [];
const all = root.querySelectorAll('*');
for (const el of all) {
if (el.shadowRoot) {
roots.push(el.shadowRoot);
roots.push(...collectShadowRoots(el.shadowRoot));
}
}
return roots;
}
let pendingWrites = [], rafId = null;
function writeUnit(unit, dst) {
if (!unit) return;
if (unit.type === 'html') {
if (unit.el?.isConnected) unit.el.innerHTML = dst;
} else if (unit.type === 'el') {
if (unit.el?.isConnected) unit.el.textContent = dst;
} else if (unit.type === 'attr') {
if (unit.el?.isConnected) unit.el.setAttribute(unit.attr, dst);
} else {
if (unit.node?.parentNode) unit.node.nodeValue = unit.head + dst + unit.tail;
}
}
function scheduleWrite(unit, dst) {
pendingWrites.push({ unit, dst });
if (!rafId) {
rafId = requestAnimationFrame(() => {
const writes = pendingWrites;
pendingWrites = [];
rafId = null;
for (const w of writes) writeUnit(w.unit, w.dst);
});
}
}
function collectUnits() {
let cacheHits = 0;
const list = [];
const processedEls = new Set();
const processedNodes = new WeakSet();
function walkTextNodes(root) {
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
let n;
while ((n = w.nextNode())) {
if (n.parentElement?.closest('pre,code,kbd,samp,var')) continue;
if (processedNodes.has(n)) continue;
const raw = n.nodeValue || '';
const text = raw.trim();
if (!should(text)) continue;
const r = rectEl(n.parentElement);
if (!r) continue;
const trimmedStart = raw.trimStart();
const head = raw.slice(0, raw.length - trimmedStart.length);
const trimmedEnd = raw.trimEnd();
const tail = raw.slice(trimmedEnd.length);
if (cache[text]) {
// 立即写入已缓存内容,恢复初版的速度感
writeUnit({ type: 'node', node: n, raw, text, head, tail, top: r.top }, cache[text]);
cacheHits++;
continue;
}
list.push({ type: 'node', node: n, raw, text, head, tail, top: r.top });
processedNodes.add(n);
if (list.length >= MAX_UNITS) return;
}
}
function walkAttrs(root) {
const ATTR_NAMES = ['title', 'placeholder', 'aria-label'];
const all = root.querySelectorAll('*');
for (const el of all) {
for (const attr of ATTR_NAMES) {
const val = (el.getAttribute(attr) || '').trim();
if (!val || !should(val)) continue;
if (cache[val]) {
// 属性立即写入
el.setAttribute(attr, cache[val]);
cacheHits++;
continue;
}
list.push({ type: 'attr', el, attr, text: val, top: 0 });
if (list.length >= MAX_UNITS) return;
}
if (list.length >= MAX_UNITS) return;
}
}
const blocks = document.querySelectorAll('p,blockquote,dd,figcaption,summary,h1,h2,h3,h4,h5,h6');
for (const el of blocks) {
if (processedEls.has(el)) continue;
if (!simpleRichBlock(el)) continue;
const trimmed = el.textContent.trim();
if (!trimmed || trimmed.length > 1600) continue;
if (!should(trimmed) && countLetters(trimmed, true) < 2) continue;
const html = el.innerHTML.trim();
const key = htmlKey(html);
if (cache[key]) {
const r = rectEl(el);
if (r) {
// 立即写入已缓存的 HTML 块
writeUnit({ type: 'html', el, text: key, top: r.top }, cache[key]);
cacheHits++;
processedEls.add(el);
}
continue;
}
const r = rectEl(el);
if (!r) continue;
processedEls.add(el);
list.push({ type: 'html', el, text: key, top: r.top });
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) processedNodes.add(node);
if (list.length >= MAX_UNITS) break;
}
const elements = document.querySelectorAll('p,li,h1,h2,h3,h4,h5,h6,dt,dd,figcaption,summary,blockquote,a');
for (const el of elements) {
if (processedEls.has(el)) continue;
if (el.tagName === 'A' && el.children.length > 0) {
if (el.querySelector('img,svg,canvas') || el.children.length > 3) continue;
} else if (el.children.length > 0) continue;
const trimmed = el.textContent.trim();
if (!should(trimmed) || trimmed.length > 1200) continue;
if (cache[trimmed]) {
const r = rectEl(el);
if (r) {
// 立即写入已缓存的纯文本元素
writeUnit({ type: 'el', el, text: trimmed, top: r.top }, cache[trimmed]);
cacheHits++;
processedEls.add(el);
}
continue;
}
const r = rectEl(el);
if (!r) continue;
processedEls.add(el);
list.push({ type: 'el', el, text: trimmed, top: r.top });
if (list.length >= MAX_UNITS) break;
}
walkTextNodes(document.body);
walkAttrs(document.body);
const shadowRoots = collectShadowRoots(document.body);
for (const shadow of shadowRoots) {
walkTextNodes(shadow);
walkAttrs(shadow);
if (list.length >= MAX_UNITS) break;
}
return { list, cacheHits };
}
function collect() {
return new Promise(resolve => {
const doCollect = () => {
const result = collectUnits();
result.list.sort((a, b) => {
const av = a.top >= 0 && a.top <= innerHeight ? 0 : 1;
const bv = b.top >= 0 && b.top <= innerHeight ? 0 : 1;
if (av !== bv) return av - bv;
return Math.abs(a.top - innerHeight / 2) - Math.abs(b.top - innerHeight / 2);
});
const out = [];
let chars = 0;
for (const unit of result.list) {
out.push(unit);
chars += unit.text.length;
if (out.length >= MAX_UNITS || chars >= MAX_CHARS) break;
}
resolve({ list: out, cacheHits: result.cacheHits });
};
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(doCollect, { timeout: 200 });
} else {
setTimeout(doCollect, 0);
}
});
}
function group(list) {
const map = {};
const texts = [];
for (const unit of list) {
const t = unit.text;
if (!map[t]) {
map[t] = [];
texts.push(t);
}
map[t].push(unit);
}
return { map, texts };
}
function chunks(texts) {
const out = [];
let cur = [], len = 0, mode = '';
for (const t of texts) {
const nextMode = t.startsWith(HTML_PREFIX) ? 'html' : 'plain';
if (cur.length && (nextMode !== mode || cur.length >= CHUNK_NODES || len + (nextMode === 'html' ? stripHtmlKey(t).length : t.length) > CHUNK_CHARS)) {
out.push(cur);
cur = [];
len = 0;
mode = '';
}
if (!cur.length) mode = nextMode;
cur.push(t);
len += nextMode === 'html' ? stripHtmlKey(t).length : t.length;
}
if (cur.length) out.push(cur);
return out;
}
function traceId() { return Date.now() + '-' + Math.random().toString(16).slice(2); }
function trans(arr, retry, cb) {
const isHtml = arr.length && arr[0].startsWith(HTML_PREFIX);
const url = isHtml ? API_HTML : API;
const body = arr.map(t => ({ Text: isHtml ? stripHtmlKey(t) : t }));
req({
method: 'POST', url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
'X-ClientTraceId': traceId()
},
data: JSON.stringify(body)
}, r => {
if ((r.status === 401 || r.status === 403) && retry) {
token = ''; tokenTime = 0;
getToken(err => { if (err) { cb(err); return; } trans(arr, false, cb); });
return;
}
if (r.status < 200 || r.status >= 300) { cb('翻译请求失败'); return; }
let data;
try { data = JSON.parse(r.responseText); } catch (e) { cb('返回格式错误'); return; }
if (!Array.isArray(data)) { cb('返回格式错误'); return; }
for (let i = 0; i < arr.length; i++) {
const dst = data[i]?.translations?.[0]?.text;
if (dst) setCache(arr[i], dst);
}
cb('');
}, e => { cb(e); });
}
function apply(g) {
let n = 0;
for (const src in g.map) {
const dst = cache[src];
if (!dst) continue;
for (const unit of g.map[src]) {
scheduleWrite(unit, dst);
n++;
}
}
return n;
}
function getToken(cb) {
if (token && Date.now() - tokenTime < 8 * 60 * 1000) { cb(''); return; }
req({ method: 'GET', url: AUTH }, r => {
const t = String(r.responseText || '').trim();
if (r.status < 200 || r.status >= 300 || !t) { cb('获取令牌失败'); return; }
token = t;
tokenTime = Date.now();
cb('');
}, cb);
}
async function startManual(callback) {
if (running) { callback?.(false, '翻译进行中'); return; }
running = true;
translateInProgress = true;
const { list, cacheHits } = await collect();
if (!list.length) {
running = false;
translateInProgress = false;
if (cacheHits > 0) {
isTranslated = true;
everTranslated = true;
callback?.(true, cacheHits);
} else {
callback?.(false, '没有需要翻译的内容');
}
return;
}
const g = group(list);
const cs = chunks(g.texts);
let index = 0, active = 0, changed = cacheHits;
const total = cs.length;
let done = 0;
updateBtnProgress(0, total);
function doneTranslate() {
const finalize = () => {
running = false;
translateInProgress = false;
if (changed > cacheHits) {
isTranslated = true;
everTranslated = true;
callback?.(true, changed);
} else {
callback?.(false, '翻译失败');
}
};
if (rafId) requestAnimationFrame(finalize);
else finalize();
}
getToken(err => {
if (err) {
running = false;
translateInProgress = false;
callback?.(false, err);
return;
}
function next() {
if (!document.body) {
running = false;
translateInProgress = false;
callback?.(false, '已停止');
return;
}
while (active < CONCURRENCY && index < cs.length) {
active++;
trans(cs[index++], true, e => {
active--;
done++;
if (e) {
if (index >= cs.length && active === 0) doneTranslate();
return;
}
changed += apply(g);
updateBtnProgress(done, total);
if (index >= cs.length && active === 0) doneTranslate();
else next();
});
}
}
next();
});
}
function restoreManual() {
if (running || translateInProgress) return;
renderOriginalFromCache();
isTranslated = false;
showRestore = false;
}
// ---------- 界面 ----------
function updateBtnProgress(num, total) {
const btn = document.getElementById('ms-manual-trans-btn');
if (!btn) return;
let pn = btn.querySelector('.ms-bp');
if (!pn) {
pn = document.createElement('span');
pn.className = 'ms-bp';
btn.appendChild(pn);
}
pn.textContent = num + '/' + total;
}
function clearBtnProgress() {
const btn = document.getElementById('ms-manual-trans-btn');
btn?.querySelector('.ms-bp')?.remove();
}
let selBtn = null, selResult = null;
function createSelectionUI() {
if (selBtn) return;
selBtn = document.createElement('button');
selBtn.id = 'ms-sel-trans-btn';
selBtn.textContent = '译';
Object.assign(selBtn.style, { position: 'fixed', display: 'none', zIndex: '999998', padding: '4px 10px', backgroundColor: '#4285f4', color: 'white', border: 'none', borderRadius: '4px' });
document.body.appendChild(selBtn);
selResult = document.createElement('div');
selResult.id = 'ms-sel-trans-result';
Object.assign(selResult.style, { position: 'fixed', display: 'none', zIndex: '999998', padding: '10px 14px', backgroundColor: 'white', color: '#333', border: '1px solid #999', maxWidth: '420px' });
document.body.appendChild(selResult);
}
function getSelText() { const sel = window.getSelection(); return sel ? sel.toString().trim() : ''; }
function showSelBtn(x, y) {
if (!selBtn || !mod().selTranslate) return;
const t = getSelText();
if (!t || t.length < 2 || ZH_RE.test(t)) { hideSelBtn(); return; }
selBtn.style.left = x + 'px';
selBtn.style.top = y + 'px';
selBtn.style.display = 'block';
}
function hideSelBtn() { if (selBtn) selBtn.style.display = 'none'; }
function showSelResult(text, x, y) {
if (!selResult) return;
selResult.textContent = text || '';
selResult.style.left = x + 'px';
selResult.style.top = y + 'px';
selResult.style.display = 'block';
}
function hideSelResult() { if (selResult) selResult.style.display = 'none'; }
function translateSelection() {
const text = getSelText();
if (!text) return;
showSelResult('翻译中…', parseInt(selResult.style.left) || 100, parseInt(selResult.style.top) || 100);
if (cache[text]) { selResult.textContent = cache[text]; return; }
trans([text], true, err => {
selResult.textContent = err ? '翻译失败' : (cache[text] || text);
});
}
function createButton() {
if (document.getElementById('ms-manual-trans-btn')) return;
// 容器 — 主按钮 + 禁止按钮作为一个整体
var box = document.createElement('div');
box.id = 'ms-btn-box';
Object.assign(box.style, { position: 'fixed', top: '50%', transform: 'translateY(-50%)', zIndex: '999999', display: 'flex', flexDirection: 'column', gap: '1px', left: '0px', transition: 'left 0.3s ease' });
const btn = document.createElement('button');
btn.id = 'ms-manual-trans-btn';
btn.textContent = '翻译';
Object.assign(btn.style, { fontSize: 'medium', padding: '10px 18px', backgroundColor: '#4285f4', color: 'white', border: 'none', borderRadius: '0 6px 0 0', cursor: 'pointer', fontFamily: 'sans-serif' });
var blockBtn = document.createElement('button');
blockBtn.id = 'ms-block-btn';
blockBtn.textContent = '禁止此站';
Object.assign(blockBtn.style, { fontSize: '12px', padding: '4px 10px', backgroundColor: '#f5f5f5', color: '#c62828', border: '1px solid #e0e0e0', borderTop: 'none', borderRadius: '0 0 6px 0', cursor: 'pointer', fontFamily: 'sans-serif', whiteSpace: 'nowrap' });
var hideLeft = '0px';
btn._isHovered = false;
btn.addEventListener('click', () => {
if (translateInProgress) return;
if (showRestore) {
restoreManual();
btn.textContent = '翻译';
btn.style.backgroundColor = '#4285f4';
syncPosition();
} else if (translateCount >= 2) {
renderTranslationFromCache();
isTranslated = true;
showRestore = true;
btn.textContent = '恢复';
btn.style.backgroundColor = '#db4437';
syncPosition();
} else {
if (!everTranslated) updateBtnProgress(0, 0);
btn.textContent = '翻译中';
btn.style.backgroundColor = '#f0ad4e';
syncPosition();
startManual((success, result) => {
clearBtnProgress();
if (success) {
translateCount++;
showRestore = true;
btn.textContent = '恢复';
btn.style.backgroundColor = '#db4437';
} else {
btn.textContent = '翻译';
btn.style.backgroundColor = '#4285f4';
console.error('翻译失败:', result);
const tip = document.createElement('div');
tip.textContent = '翻译失败: ' + (result || '未知错误');
Object.assign(tip.style, { position: 'fixed', bottom: '90px', left: '20px', backgroundColor: 'rgba(0,0,0,0.7)', color: 'white', padding: '6px 12px', borderRadius: '6px', fontSize: '12px', zIndex: '999999' });
document.body.appendChild(tip);
setTimeout(() => tip.remove(), 2000);
}
syncPosition();
});
}
});
// 块按钮事件
blockBtn.addEventListener('mouseenter', function() { blockBtn.style.backgroundColor = '#e0e0e0'; });
blockBtn.addEventListener('mouseleave', function() { blockBtn.style.backgroundColor = '#f5f5f5'; });
blockBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (blockBtn._blocked) return;
blockBtn._blocked = true;
var host = location.hostname;
var cur = (mod().blockedSites || '').split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
var wildcard = '*.' + host.split('.').slice(-2).join('.');
if (cur.some(function(b) { return host === b || (b.startsWith('*.') && host.endsWith(b.slice(2))); })) {
blockBtn.textContent = '✅ 已禁止'; setTimeout(function() { blockBtn.textContent = '禁止此站'; blockBtn._blocked = false; }, 1500); return;
}
cur.push(wildcard);
settings.modules.translate.blockedSites = cur.join('\n');
saveSettings();
blockBtn.textContent = '✅ 已禁止';
setTimeout(function() { blockBtn.textContent = '禁止此站'; blockBtn._blocked = false; }, 1500);
});
box.appendChild(btn);
box.appendChild(blockBtn);
document.body.appendChild(box);
var hideLeft = '0px';
function syncPosition() {
if (showRestore || translateInProgress || (mod().btnMode === 'show' && everTranslated)) { box.style.left = '0px'; return; }
box.style.left = btn._isHovered ? '0px' : hideLeft;
}
function updateHideLeft() {
const showEdge = 10;
hideLeft = -(box.offsetWidth - showEdge) + 'px';
syncPosition();
}
requestAnimationFrame(function(){updateHideLeft();});
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(function(){updateHideLeft();}).observe(box);
box.addEventListener('mouseenter', function(){btn._isHovered = true; syncPosition();});
box.addEventListener('mouseleave', function(){btn._isHovered = false; syncPosition();});
}
function initSelectionEvents() {
document.addEventListener('mouseup', function(e) {
if (e.target.id === 'ms-manual-trans-btn' || e.target.id === 'ms-sel-trans-btn' || selResult?.contains(e.target)) return;
setTimeout(() => {
if (!mod().selTranslate) { hideSelBtn(); return; }
const text = getSelText();
if (!text || text.length < 2 || ZH_RE.test(text)) { hideSelBtn(); return; }
const range = window.getSelection().getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect && (rect.width > 0 || rect.height > 0)) showSelBtn(rect.right + 4, rect.top - 30);
}, 10);
});
document.addEventListener('mousedown', function(e) {
if (e.target.id === 'ms-sel-trans-btn' || selResult?.contains(e.target)) return;
hideSelBtn();
hideSelResult();
});
selBtn?.addEventListener('click', function(e) {
e.stopPropagation();
const rect = selBtn.getBoundingClientRect();
showSelResult('翻译中…', rect.left, rect.bottom + 4);
translateSelection();
});
}
function matchShortcut(combo, e) {
if (!combo) return false;
const parts = combo.split('+');
const key = parts.pop();
const mods = { Ctrl: false, Alt: false, Shift: false };
for (const p of parts) mods[p] = true;
if (mods.Ctrl !== e.ctrlKey || mods.Alt !== e.altKey || mods.Shift !== e.shiftKey) return false;
if (/^[A-Z]$/.test(key)) return e.code === 'Key' + key;
if (/^\d$/.test(key)) return e.code === 'Digit' + key;
return e.code === key || e.key === key;
}
function initShortcuts() {
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
if (matchShortcut(mod().pageShortcut, e)) {
e.preventDefault();
document.getElementById('ms-manual-trans-btn')?.click();
return;
}
if (matchShortcut(mod().selShortcut, e)) {
if (!mod().selTranslate) return;
e.preventDefault();
const text = getSelText();
if (text && text.length >= 2 && !ZH_RE.test(text)) {
if (!selBtn) { createSelectionUI(); initSelectionEvents(); }
selResult.textContent = '翻译中…';
selResult.style.display = 'block';
const rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
selResult.style.left = rect.left + 'px';
selResult.style.top = (rect.bottom + 4) + 'px';
translateSelection();
}
return;
}
});
}
function tryAutoTranslate() {
if (!mod().autoTranslate) return;
const doit = () => {
setTimeout(() => {
const btn = document.getElementById('ms-manual-trans-btn');
if (btn && !isTranslated) btn.click();
}, 1500);
};
if (document.readyState === 'complete') doit();
else window.addEventListener('load', doit);
}
function init() {
if (!settings.pageEnabled.translate) return;
var blocked = (mod().blockedSites || '').split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
for (var i = 0; i < blocked.length; i++) { if (location.hostname === blocked[i] || (blocked[i].startsWith('*.') && location.hostname.endsWith(blocked[i].slice(1)))) return; }
if (!document.getElementById('ms-bi-style')) { var s = document.createElement('style'); s.id = 'ms-bi-style'; s.textContent = '.ms-bi-gap{color:#aaa}.ms-bi-trans{background:#fffbe6;border-radius:2px;padding:0 2px;color:#d4380d;font-weight:500}'; document.head.appendChild(s); }
getToken(function(){});
createButton();
if (mod().selTranslate) { createSelectionUI(); initSelectionEvents(); }
initShortcuts();
tryAutoTranslate();
}
function destroy() { var bx = document.getElementById('ms-btn-box'); if (bx) bx.remove(); if (selBtn) { selBtn.remove(); selResult.remove(); selBtn = null; selResult = null; } if (isTranslated) renderOriginalFromCache(); isTranslated = false; translateInProgress = false; everTranslated = false; }
function buildTranslatePage() {
var m = mod();
var esc = function(v) { return String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); };
function dual(key, t, f, val) { return ''; }
return '\
⚙️ 翻译' + dual('selTranslate','开启','关闭',m.selTranslate) + '
' + dual('autoTranslate','开启','关闭',m.autoTranslate) + '
自动翻译:页面加载完成后自动翻译全文。
\
🔘 翻译按钮左侧边缘悬停显示翻译按钮。
\
\
🚫 禁止站点
每行一个域名。禁止后翻译按钮不会出现在这些网站上。
';
}
function bindPanelEvents() {
document.querySelectorAll('.wp-trans-sel').forEach(function(el) { el.addEventListener('change', function() { var v = this.value === 'true' ? true : (this.value === 'false' ? false : this.value); settings.modules.translate[this.dataset.key] = v; saveSettings(); }); });
document.querySelectorAll('.wp-dual[data-key]').forEach(function(c) { c.querySelectorAll('.wp-dual-btn').forEach(function(b) { b.addEventListener('click', function() { var v = this.dataset.value === 'true'; settings.modules.translate[c.dataset.key] = v; saveSettings(); c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); }); }); }); });
document.querySelectorAll('.wp-trans-key').forEach(function(inp) { inp.addEventListener('focus', function() { this.value = ''; this._cap = true; }); inp.addEventListener('blur', function() { this._cap = false; }); });
var bt = document.getElementById('wp-trans-blocked'); if (bt) bt.addEventListener('input', function() { settings.modules.translate.blockedSites = this.value; saveSettings(); });
var bb = document.getElementById('wp-trans-block-site'); if (bb && bt) bb.addEventListener('click', function() { var h = location.hostname; var cur = bt.value; var lines = cur.split('\n').map(function(s) { return s.trim(); }).filter(Boolean); var newLines = lines.filter(function(b) { return h !== b && !(b.startsWith('*.') && h.endsWith(b.slice(1))); }); if (newLines.length < lines.length) { bt.value = newLines.join('\n'); settings.modules.translate.blockedSites = bt.value; saveSettings(); } });
}
document.addEventListener('wp-module-toggle', function(e) { if (e.detail.page === 'translate') { if (e.detail.enabled) init(); else destroy(); } });
// ─── 导出 ───
return {
init: init,
destroy: destroy,
buildPage: buildTranslatePage,
bindPanelEvents: bindPanelEvents,
toggleBilingual: function() { if (!isTranslated) return; if (currentMode === MODE_BI) renderTranslationFromCache(); else renderBilingual(); }
};
})();
const BILI_MODULE = (function() {
function mod() { return settings.modules.bilibili || {}; }
// ─── IP 属地显示 ───
function startIP() {
if (mod().showIp === false) return;
var code = '(' + function() {
var origFetch = window.fetch;
window.fetch = function() {
var url = (typeof arguments[0] === 'string') ? arguments[0] : (arguments[0] && arguments[0].url) || '';
if (url.indexOf('/x/v2/reply/wbi/main') === -1 && url.indexOf('/x/v2/reply/reply') === -1) return origFetch.apply(this, arguments);
return origFetch.apply(this, arguments).then(function(r) {
try {
var clone = r.clone();
return clone.json().then(function(json) {
if (!json || !json.data) return r;
function process(c) {
if (c && c.reply_control && c.reply_control.location && c.member) {
var loc = c.reply_control.location.replace(/IP属地:?/ig, '').trim();
if (loc) c.member.uname += ' (' + loc + ')';
}
if (c && c.replies) for (var ri = 0; ri < c.replies.length; ri++) process(c.replies[ri]);
}
var lists = [json.data.top_replies, json.data.replies, json.data.root];
for (var li = 0; li < lists.length; li++) {
if (Array.isArray(lists[li])) for (var ci = 0; ci < lists[li].length; ci++) process(lists[li][ci]);
}
return new Response(JSON.stringify(json), { status: r.status, statusText: r.statusText, headers: r.headers });
});
} catch(e) { return r; }
});
};
} + ')();';
var s = document.createElement('script');
s.textContent = code;
(document.head || document.documentElement).appendChild(s);
s.remove();
}
// ─── 干净链接 ───
function startClean() {
if (mod().cleanLinks === false) return;
var P = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id','fbclid','gclid','msclkid','twclid','_ga','_gl','ref','source','via','share','from','spm'];
var S = {};
// 合并外部来源规则
var ext = mod().cleanSources || {};
for (var url in ext) { var src = ext[url]; if (!src || !src.enabled || !src.data) continue;
var providers = src.data.providers || src.data;
for (var key in providers) { var rule = providers[key]; if (rule.rules) {
if (!S[key]) S[key] = [];
for (var ri = 0; ri < rule.rules.length; ri++) { var p = rule.rules[ri].replace(/^\(?:\?%3F\)?\?/,''); if (S[key].indexOf(p) === -1) S[key].push(p); }
} }
}
function getParams(h) {
var a = P.slice();
for (var k in S) { var ds = k.split(','); for (var i = 0; i < ds.length; i++) { if (h.indexOf(ds[i]) !== -1) { a = a.concat(S[k]); break; } } }
return a;
}
function clean(u) {
try { var o = new URL(u, location.href); var ps = getParams(o.hostname); var c = false;
for (var i = 0; i < ps.length; i++) { if (o.searchParams.has(ps[i])) { o.searchParams.delete(ps[i]); c = true; } }
return c ? o.toString() : u;
} catch(e) { return u; }
}
// 拦截 history.pushState 和 replaceState(B站通过它加 ?vd_source=)
var origPush = history.pushState;
history.pushState = function(s, t, u) { return origPush.call(this, s, t, u ? clean(String(u)) : u); };
var origReplace = history.replaceState;
history.replaceState = function(s, t, u) { return origReplace.call(this, s, t, u ? clean(String(u)) : u); };
// 定期检查地址栏(B站可能通过 location.href 跳转)
var lastUrl = location.href;
setInterval(function() {
try {
var cur = clean(location.href);
if (cur !== location.href) { history.replaceState(null, '', cur); lastUrl = cur; }
} catch(e) {}
}, 500);
// 清理所有链接
function cleanAll() {
var links = document.querySelectorAll('a[href]');
for (var i = 0; i < links.length; i++) { try { var h = links[i].getAttribute('href'); var ch = clean(h); if (ch !== h) links[i].setAttribute('href', ch); } catch(e) {} }
}
cleanAll();
new MutationObserver(function() { cleanAll(); }).observe(document.body, { childList: true, subtree: true });
document.addEventListener('click', function(e) {
var t = e.target; while (t && t.tagName !== 'A') t = t.parentNode;
if (!t) return; var h = t.getAttribute('href'); var ch = clean(h); if (ch !== h) t.setAttribute('href', ch);
}, true);
}
function loadCleanSource(url, cb) {
if (!url) { cb('请输入 URL'); return; }
function extractName(url) {
var source = '';
if (url.indexOf('raw.githubusercontent.com') > -1) source = 'github - ';
else if (url.indexOf('gitee.com') > -1) source = 'gitee - ';
var m;
m = url.match(/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/(.+)/);
if (m) return source + m[1] + '/' + m[2] + '/' + m[3];
m = url.match(/gitee\.com\/([^/]+)\/([^/]+)\/raw\/([^/]+)\/(.+)/);
if (m) return source + m[1] + '/' + m[2] + '/' + m[4];
return source + url.split('/').pop();
}
function onOk(t) {
// 去掉开头的注释行(// 前缀的整行)
var lines = t.split('\n');
if (lines.length > 1 && lines[0].trim().indexOf('//') === 0) { lines.shift(); t = lines.join('\n'); }
try { var data = JSON.parse(t); } catch(e) { cb('JSON 解析失败'); return; }
var srcs = settings.modules.bilibili.cleanSources || {}; srcs[url] = { name: extractName(url), enabled: true, data: data }; settings.modules.bilibili.cleanSources = srcs; saveSettings(); cb(null, Object.keys(data.providers || data).length);
}
if (typeof GM_xmlhttpRequest === 'function') {
GM_xmlhttpRequest({ method: 'GET', url: url + (url.indexOf('?') > -1 ? '&' : '?') + '_t=' + Date.now(), onload: function(r) { if (r.status >= 200 && r.status < 300) onOk(r.responseText); else cb('HTTP ' + r.status); }, onerror: function() { cb('网络错误(检查 @connect 授权)'); }, ontimeout: function() { cb('请求超时'); }, timeout: 12000 });
} else if (typeof GM !== 'undefined' && GM.xmlHttpRequest) {
GM.xmlHttpRequest({ method: 'GET', url: url + (url.indexOf('?') > -1 ? '&' : '?') + '_t=' + Date.now() }).then(function(r) { if (r.status >= 200 && r.status < 300) onOk(r.responseText); else cb('HTTP ' + r.status); }).catch(function() { cb('网络错误'); });
} else {
fetch(url + (url.indexOf('?') > -1 ? '&' : '?') + '_t=' + Date.now()).then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }).then(onOk).catch(function(e) { cb('加载失败: ' + (e.message || '网络错误')); });
}
}
function startDirectLink() {
if (mod().directLink === false) return;
var rules = [
[/link\.zhihu\.com.*target=/, function() { var m = location.href.match(/target=([^&]+)/); if (m) location.href = decodeURIComponent(m[1]); }],
[/link\.csdn\.net.*target=/, function() { var m = location.href.match(/target=([^&]+)/); if (m) location.href = decodeURIComponent(m[1]); }],
[/jianshu\.com\/go-wild.*url=/, function() { var m = location.href.match(/url=([^&]+)/); if (m) location.href = decodeURIComponent(m[1]); }],
[/link\.juejin\.cn.*target=/, function() { var m = location.href.match(/target=([^&]+)/); if (m) location.href = decodeURIComponent(m[1]); }],
[/mail\.qq\.com.*gourl=/, function() { var p = new URL(location.href).searchParams.get('gourl'); if (p) location.href = decodeURIComponent(p); }],
[/\.google\.com\/url\?/, function() { var p = new URL(location.href).searchParams.get('url') || new URL(location.href).searchParams.get('q'); if (p) location.href = p; }],
];
for (var i = 0; i < rules.length; i++) { if (rules[i][0].test(location.href)) { rules[i][1](); return; } }
}
var _hlObserver = null;
function removeHighlight() {
if (_hlObserver) { _hlObserver.disconnect(); _hlObserver = null; }
document.querySelectorAll('.wp-hl').forEach(function(el) {
var p = el.parentNode;
if (p) {
var txt = document.createTextNode(el.textContent);
p.replaceChild(txt, el);
p.normalize();
}
});
var st = document.getElementById('wp-hl-style');
if (st) st.remove();
}
function startHighlight() {
if (!mod().highlightEnabled) return;
var kw = mod().highlightKeywords || '';
var clr = mod().highlightColor || '#FF0000';
if (!kw) return;
var keywords = kw.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
if (!keywords.length) return;
// 样式
var st = document.getElementById('wp-hl-style');
if (!st) { st = document.createElement('style'); st.id = 'wp-hl-style'; document.head.appendChild(st); }
st.textContent = '.wp-hl{background:' + clr + '!important;border-radius:2px;padding:0 1px}';
// 高亮单个文本节点
function highlightNode(n) {
if (!n || n.nodeType !== 3 || !n.parentNode) return;
if (n.parentNode.closest('pre,code,kbd,samp,var,script,style,textarea,.wp-hl')) return;
var text = n.nodeValue;
for (var ki = 0; ki < keywords.length; ki++) {
try {
var kw = keywords[ki];
var re;
if (kw.startsWith('/') && kw.lastIndexOf('/') > 0) {
var lastSlash = kw.lastIndexOf('/');
var pattern = kw.substring(1, lastSlash);
var flags = kw.substring(lastSlash + 1) || 'gi';
re = new RegExp('(' + pattern + ')', flags);
} else {
re = new RegExp('(' + kw.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + ')', 'gi');
}
if (re.test(text)) {
re.lastIndex = 0;
text = text.replace(re, '$1');
n.parentNode.replaceChild(function(){var s=document.createElement('span');s.innerHTML=text;return s;}(), n);
return;
}
} catch(e) {}
}
}
function walk() {
var w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
var nodes = [];
while (w.nextNode()) nodes.push(w.currentNode);
for (var i = 0; i < nodes.length; i++) highlightNode(nodes[i]);
}
// 断开旧 observer
if (_hlObserver) _hlObserver.disconnect();
walk();
_hlObserver = new MutationObserver(function() { walk(); });
_hlObserver.observe(document.body, { childList: true, subtree: true });
}
var _preloadReady = false;
function startPreload() {
if (mod().preloadBoost === false) return;
if (_preloadReady) return;
_preloadReady = true;
// 1. 图片预加载:将懒加载图片改为立即加载
document.querySelectorAll('img[loading="lazy"]').forEach(function(img) { img.loading = 'eager'; });
document.querySelectorAll('img').forEach(function(img) {
var attrs = ['data-src','data-srcset','data-lazy-src','data-lazy-srcset','data-original','data-original-src','data-bg'];
for (var ai = 0; ai < attrs.length; ai++) {
if (img.hasAttribute(attrs[ai])) {
var v = img.getAttribute(attrs[ai]);
if (v && (img.src.indexOf('data:image') === 0 || img.src !== v || !img.complete)) {
if (attrs[ai].indexOf('srcset') > -1) img.srcset = v; else img.src = v;
}
img.removeAttribute(attrs[ai]);
break;
}
}
});
// MutationObserver 监控新图片
new MutationObserver(function() {
document.querySelectorAll('img[loading="lazy"]').forEach(function(img) { img.loading = 'eager'; });
document.querySelectorAll('img').forEach(function(img) {
var attrs2 = ['data-src','data-srcset','data-lazy-src','data-original','data-bg'];
for (var ai2 = 0; ai2 < attrs2.length; ai2++) {
if (img.hasAttribute(attrs2[ai2])) { var v2 = img.getAttribute(attrs2[ai2]); if (v2) { if (attrs2[ai2].indexOf('srcset') > -1) img.srcset = v2; else img.src = v2; } img.removeAttribute(attrs2[ai2]); break; }
}
});
}).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['loading','src','data-src'] });
// 2. 悬停预读
var delay = mod().hoverDelay || 100;
var timer = null;
document.addEventListener('mouseover', function(e) {
var a = e.target.closest('a');
if (!a || !a.href || a.href.indexOf('http') !== 0) return;
if (/login|logout|register|signin|signup|pay|download|delete/.test(a.pathname)) return;
if (timer) { clearTimeout(timer); timer = null; }
timer = setTimeout(function() {
var link = document.querySelector('link[rel="prefetch"][href="' + a.href.replace(/"/g,'') + '"]');
if (!link) { var l = document.createElement('link'); l.rel = 'prefetch'; l.href = a.href; document.head.appendChild(l); }
}, delay);
}, { passive: true });
document.addEventListener('mouseout', function() { if (timer) { clearTimeout(timer); timer = null; } }, { passive: true });
}
var colorStyleId = 'wp-color-mode';
var colorModes = {
'默认': '',
'夜间模式': 'html{filter:invert(0.88)hue-rotate(180deg)!important}img,video,canvas,svg,iframe,[style*="background-image"]{filter:invert(1)hue-rotate(180deg)!important}',
'护眼模式': 'html{filter:sepia(0.35)hue-rotate(340deg)!important}',
'灰度模式': 'html{filter:grayscale(1)!important}',
'高对比度': 'html{filter:contrast(1.5)brightness(1.1)!important}',
'复古模式': 'html{filter:sepia(0.4)contrast(0.9)brightness(0.95)!important}',
};
function applyColorMode() {
var mode = mod().colorMode || '默认';
var existing = document.getElementById(colorStyleId);
if (existing) existing.remove();
if (mode === '默认') return;
if (colorModes[mode]) {
var s = document.createElement('style');
s.id = colorStyleId;
s.textContent = colorModes[mode];
document.documentElement.appendChild(s);
}
}
var _scrollbarStyleId = 'wp-sb-style';
function applyScrollbar() {
var style = mod().scrollbarStyle || 'default';
var old = document.getElementById(_scrollbarStyleId);
if (style === 'default') {
if (old) old.remove();
return;
}
var thumb = mod().scrollbarColor || '#FF0000';
var css = '::-webkit-scrollbar{width:8px;height:8px}' +
'::-webkit-scrollbar-thumb{background:' + thumb + ';border-radius:4px}' +
'::-webkit-scrollbar-track{background:transparent}' +
'html{scrollbar-color:' + thumb + ' transparent;scrollbar-width:thin}';
var st = old || document.createElement('style');
st.id = _scrollbarStyleId;
st.textContent = css;
if (!old) document.head.appendChild(st);
}
function startScrollToTop() {
if (!mod().backToTop) return;
if (document.getElementById('wp-scroll-btns')) return;
var div = document.createElement('div');
div.id = 'wp-scroll-btns';
div.innerHTML = '' +
'';
div.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:4px';
// 全屏时隐藏
function fsHandler() { div.style.display = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement ? 'none' : 'flex'; }
document.addEventListener('fullscreenchange', fsHandler);
document.addEventListener('webkitfullscreenchange', fsHandler);
document.addEventListener('mozfullscreenchange', fsHandler);
var btns = div.children;
btns[0].addEventListener('click', function() { window.scrollTo({ top: 0, behavior: 'smooth' }); });
btns[1].addEventListener('click', function() { window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); });
document.body.appendChild(div);
}
var _spacingNodes = null;
function applySpacing() {
// 清除之前的修改
if (_spacingNodes) {
for (var si = 0; si < _spacingNodes.length; si++) {
var sn = _spacingNodes[si];
if (sn.el && sn.el.parentNode && sn.orig !== undefined) sn.el.nodeValue = sn.orig;
}
}
_spacingNodes = null;
if (!mod().spacingEnabled) return;
var saved = [];
var w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
var n, re1 = /([\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff])([^\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\s\.\,\;\:\!\?\(\)\[\]\{\}\u3000-\u303f\uff00-\uffef\u2000-\u206f])/g, re2 = /([^\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\s\.\,\;\:\!\?\(\)\[\]\{\}\u3000-\u303f\uff00-\uffef\u2000-\u206f])([\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff])/g;
while ((n = w.nextNode())) {
if (n.parentElement && n.parentElement.closest('pre,code,kbd,samp,var,script,style')) continue;
var old = n.nodeValue;
var nw = old.replace(re1, '$1 $2').replace(re2, '$1 $2');
if (nw !== old) { n.nodeValue = nw; saved.push({ el: n, orig: old }); }
}
_spacingNodes = saved;
}
var _unlockStyle = null, _unlockApplied = false;
function applyUnlock() {
if (_unlockApplied) return;
_unlockApplied = true;
if (!mod().unlockEnabled) { _unlockApplied = false; return; }
// 拦截事件(capture 阶段)
['contextmenu','copy','cut','dragstart'].forEach(function(ev) {
document.addEventListener(ev, function(e) { e.stopPropagation(); e.preventDefault(); }, true);
});
// 覆盖 DOM0 事件
document.oncontextmenu = null; document.onselectstart = null; document.oncopy = null;
// 覆盖 CSS user-select
if (!_unlockStyle) { _unlockStyle = document.createElement('style'); _unlockStyle.id = 'wp-unlock-style'; document.head.appendChild(_unlockStyle); }
_unlockStyle.textContent = '* { user-select: auto !important; -webkit-user-select: auto !important; } body, body * { -webkit-touch-callout: default !important; }';
// 移除内联 user-select 样式
document.querySelectorAll('[style*="user-select"],[style*="user-select"]').forEach(function(el) {
el.style.setProperty('user-select', 'auto', 'important');
el.style.setProperty('-webkit-user-select', 'auto', 'important');
});
}
function init() {
applyColorMode();
applyScrollbar();
startDirectLink();
startPreload();
startHighlight();
startScrollToTop();
applySpacing();
applyUnlock();
var defaultUrls = ['https://gitee.com/qiuzongman/WebPlus/raw/master/ClearURLs/me.json','https://gitee.com/qiuzongman/WebPlus/raw/master/ClearURLs/clearURLs.json'];
var srcs = mod().cleanSources || {};
var needLoad = false;
if (mod().cleanLinks !== false) {
for (var ui = 0; ui < defaultUrls.length; ui++) { if (!srcs[defaultUrls[ui]]) { needLoad = true; loadCleanSource(defaultUrls[ui], function() { startClean(); }); } }
}
if (!needLoad) startClean();
if (location.hostname.indexOf('bilibili.com') === -1) return;
startIP();
}
function refreshCleanSourceList(list) {
if (!list) return;
var srcs = mod().cleanSources || {};
var esc = function(v) { return String(v).replace(/&/g,'&').replace(//g,'>'); };
var html = '';
for (var url in srcs) {
var src = srcs[url];
var count = src.data ? Object.keys(src.data.providers || src.data).length : 0;
html += '
';
}
if (!html) html = '暂无外部规则。建议加载 ClearURLs 规则。
';
list.innerHTML = html;
// 绑定事件
list.querySelectorAll('.wp-clean-source-enable').forEach(function(cb) {
cb.addEventListener('change', function() {
var srcs = settings.modules.bilibili.cleanSources || {};
if (srcs[this.dataset.url]) { srcs[this.dataset.url].enabled = this.checked; saveSettings(); }
});
});
list.querySelectorAll('.wp-clean-source-remove').forEach(function(b) {
b.addEventListener('click', function() {
var srcs = settings.modules.bilibili.cleanSources || {};
delete srcs[this.dataset.url];
settings.modules.bilibili.cleanSources = srcs; saveSettings();
refreshCleanSourceList(list);
});
});
}
function bindPanelEvents() {
function bindDual(id, key) {
var c = document.getElementById(id);
if (!c) return;
c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.modules.bilibili[key] = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); });
});
});
}
bindDual('wp-bili-ip', 'showIp');
bindDual('wp-direct-link', 'directLink');
bindDual('wp-preload', 'preloadBoost');
var hd = document.getElementById('wp-hover-delay');
if (hd) hd.addEventListener('change', function() { settings.modules.bilibili.hoverDelay = parseInt(this.value) || 100; saveSettings(); });
var cm = document.getElementById('wp-color-mode-select');
if (cm) cm.addEventListener('change', function() { settings.modules.bilibili.colorMode = this.value; saveSettings(); applyColorMode(); });
// 自定义高亮开关,需要即时生效
(function() {
var c = document.getElementById('wp-hl-switch');
if (!c) return;
c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.modules.bilibili.highlightEnabled = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); });
removeHighlight(); if (v) startHighlight();
});
});
})();
var hlKeywords = document.getElementById('wp-hl-keywords');
if (hlKeywords) hlKeywords.addEventListener('change', function() {
settings.modules.bilibili.highlightKeywords = this.value;
saveSettings();
removeHighlight(); startHighlight();
});
var hlColor = document.getElementById('wp-hl-color');
if (hlColor) hlColor.addEventListener('change', function() {
settings.modules.bilibili.highlightColor = this.value;
saveSettings();
removeHighlight(); startHighlight();
});
// 中英间距
(function() {
var c = document.getElementById('wp-spacing');
if (c) c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.modules.bilibili.spacingEnabled = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); });
applySpacing();
});
});
})();
bindDual('wp-clean-links', 'cleanLinks');
var sc = document.getElementById('wp-scrollbar-color');
if (sc) sc.addEventListener('change', function() { settings.modules.bilibili.scrollbarColor = this.value; saveSettings(); applyScrollbar(); });
// 滚动条滑块颜色
(function() {
var c = document.getElementById('wp-scrollbar-style');
if (c) c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value;
settings.modules.bilibili.scrollbarStyle = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === v); });
var cp = document.getElementById('wp-scrollbar-color');
if (cp) cp.style.opacity = v === 'default' ? '0.4' : '1';
applyScrollbar();
});
});
})();
// 顶底按钮
(function() {
var c = document.getElementById('wp-scrolltop-switch');
if (c) c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.modules.bilibili.backToTop = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); });
if (v) startScrollToTop(); else {
var el = document.getElementById('wp-scroll-btns');
if (el) el.remove();
}
});
});
})();
// 解除文本限制
(function() {
var c = document.getElementById('wp-unlock');
if (c) c.querySelectorAll('.wp-dual-btn').forEach(function(b) {
b.addEventListener('click', function() {
var v = this.dataset.value === 'true';
settings.modules.bilibili.unlockEnabled = v; saveSettings();
c.querySelectorAll('.wp-dual-btn').forEach(function(x) { x.classList.toggle('active', x.dataset.value === String(v)); });
if (v) applyUnlock(); else { _unlockApplied = false; var us = document.getElementById('wp-unlock-style'); if (us) us.remove(); }
});
});
})();
// 加载外部清洗规则
var cleanLoadBtn = document.getElementById('wp-clean-load');
var cleanUrlInput = document.getElementById('wp-clean-source-url');
var cleanList = document.getElementById('wp-clean-source-list');
if (cleanLoadBtn && cleanUrlInput) {
cleanLoadBtn.addEventListener('click', function() {
var url = cleanUrlInput.value.trim();
if (!url) return;
cleanLoadBtn.disabled = true;
cleanLoadBtn.textContent = '…';
loadCleanSource(url, function(err, count) {
cleanLoadBtn.disabled = false;
cleanLoadBtn.textContent = '加载';
if (err) { alert(err); return; }
refreshCleanSourceList(cleanList);
});
});
}
if (cleanList) refreshCleanSourceList(cleanList);
}
document.addEventListener('wp-panel-open', function() { bindPanelEvents(); });
return { init: init, destroy: function() {}, buildPage: function(){return'';}, bindPanelEvents: bindPanelEvents, loadCleanSource: loadCleanSource };
})();
// ===================================================================
var _origBuild = buildPageContent;
var _esc = function(v) { return String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); };
buildPageContent = function(pageId) {
if (pageId === 'translate') return TRANS_MODULE.buildPage();
if (pageId === 'other') {
var m = settings.modules.bilibili || {};
return '\
\
📺 B站\
\
拦截评论 API 响应,在用户名后显示 IP 属地。
\
\
\
🔓 解除文本限制\
\
解除禁止复制、选择文本、右键菜单的限制。
\
';
}
if (pageId === 'visual') {
var m = settings.modules.bilibili || {};
return '\
\
🔍 关键词高亮\
\
\
\
\
\
🔤 文本混排间隔\
\
在中外文字/数字/符号之间自动添加空格,提升阅读体验。
\
\
\
🎨 网页颜色\
\
\
\
🖱️ 滚动条\
\
\
';
}
if (pageId === 'speed') {
var m = settings.modules.bilibili || {};
return '\
\
⚡ 网页预加载\
\
ms
\
图片懒加载提前 + 鼠标悬停预读链接(点击即开)。
\
\
\
🔗 链接直达\
\
绕过知乎等网站的中转页面,直达目标链接。
\
\
\
🔗 链接净化\
\
清除 URL 中的跟踪参数。
\
载入外部规则(从 Gitee/GitHub):
\
\
\
\
\
\
';
}
if (pageId === 'global') return buildGlobalPage();
if (pageId === 'about') {
return '\
\
\
Web+ v1.0.1
\
\
qiuzongman@foxmail.com
\
MIT
\
\
\
\
\
\
\
🫶 支援我买 Token 继续改进代码\
\
微信
\

\
支付宝
\

\
\
';
}
return _origBuild(pageId);
};
function buildGlobalPage() {
var esc = function(v) { return String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); };
return `
⚙️ 设置
将清空所有模块的配置,恢复为初始状态。此操作不可撤销。
`;
}
function matchShortcut(combo, e) {
if (!combo) return false;
var parts = combo.split('+'), key = parts.pop(), mods = { Ctrl: false, Alt: false, Shift: false };
parts.forEach(function(p) { mods[p] = true; });
if (mods.Ctrl !== e.ctrlKey || mods.Alt !== e.altKey || mods.Shift !== e.shiftKey) return false;
if (/^[A-Z]$/.test(key)) return e.code === 'Key' + key;
if (/^\d$/.test(key)) return e.code === 'Digit' + key;
return e.code === key || e.key === key;
}
// ===================================================================
// 初始化
// ===================================================================
function init() {
loadSettings();
// 设置入口 — 始终注册
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('设置', openSettings);
}
// 全局快捷键 — 打开设置面板
document.addEventListener('keydown', function(e) {
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) return;
// 用户自定义快捷键
if (settings.openSettingsKey && matchShortcut(settings.openSettingsKey, e)) {
e.preventDefault();
openSettings();
}
// 备用快捷键 Ctrl+Shift+. 始终可用
if (e.ctrlKey && e.shiftKey && e.key === '.') {
e.preventDefault();
openSettings();
}
// 双语对照快捷键
var bk = settings.modules.translate && settings.modules.translate.bilingualKey;
if (bk && matchShortcut(bk, e)) {
e.preventDefault();
if (typeof TRANS_MODULE !== 'undefined') TRANS_MODULE.toggleBilingual();
}
});
// 初始化各模块
TRANS_MODULE.init();
BILI_MODULE.init();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();