// ==UserScript==
// @name Watermark Remover Suite
// @namespace https://blog.wayneshao.com/
// @version 1.1
// @description 通用低风险去水印、站点专用处理(含 ZSXQ)、以及手动高风险清理按钮(支持拖动贴边与位置记忆)
// @author You
// @match *://*/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* ========== 全局配置 ========== */
const BUTTON_ID = 'wm-remover-sweep-btn';
const BASE_STYLE_ID = 'wm-remover-base-style';
const ZSXQ_STYLE_ID = 'wm-remover-zsxq-style';
const BUTTON_STORAGE_KEY = 'wm-remover-button-pos-v1';
const isZsxqDomain = /(^|\.)zsxq\.com$/i.test(window.location.hostname);
/* ========== 全局状态 ========== */
let sweepButton = null;
let suppressClick = false;
const dragState = {
active: false,
moved: false,
pointerId: null,
startX: 0,
startY: 0,
};
let buttonPos = loadButtonPosition();
const lowRiskObserver = new MutationObserver(handleLowRiskMutations);
const specialHandlers = [
{
name: 'zsxq',
test: () => isZsxqDomain,
init: setupZsxqHandler,
},
];
/* ========== 初始化入口 ========== */
injectBaseCss();
whenReady(() => {
ensureSweepButton();
startLowRiskLogic();
runSpecialHandlers();
});
/* ========== 通用低风险逻辑 ========== */
function startLowRiskLogic() {
lowRiskSweep(document);
const startObserver = () => {
if (!document.body) return false;
lowRiskObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style'],
});
return true;
};
if (!startObserver()) {
const watcher = new MutationObserver(() => {
if (startObserver()) watcher.disconnect();
});
watcher.observe(document.documentElement, { childList: true });
}
}
function handleLowRiskMutations(mutations) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => lowRiskSweep(node));
} else if (mutation.type === 'attributes') {
lowRiskProcessElement(mutation.target);
}
}
}
function lowRiskSweep(root) {
if (!root) return;
const startNode = root instanceof Document ? root.documentElement : root;
walkDom(startNode, lowRiskProcessElement);
}
function lowRiskProcessElement(el) {
if (!(el instanceof Element) || shouldSkipButton(el)) return;
const inlineStyle = el.getAttribute('style');
if (inlineStyle && /nullbackground/i.test(inlineStyle)) {
el.setAttribute('style', inlineStyle.replace(/nullbackground/gi, 'background'));
}
const backgroundImage = el.style.getPropertyValue('background-image');
if (backgroundImage && /url\(\s*data:image/i.test(backgroundImage)) {
el.style.setProperty('background-image', 'none', 'important');
}
const background = el.style.getPropertyValue('background');
if (background && /url\(\s*data:image/i.test(background)) {
el.style.setProperty(
'background',
background.replace(/url\([^)]*\)/gi, 'none').trim(),
'important'
);
}
const maskImage =
el.style.getPropertyValue('mask-image') ||
el.style.getPropertyValue('-webkit-mask-image');
if (maskImage && /url\(\s*data:image/i.test(maskImage)) {
el.style.setProperty('mask-image', 'none', 'important');
el.style.setProperty('-webkit-mask-image', 'none', 'important');
}
}
/* ========== 站点专用逻辑(目前仅 ZSXQ) ========== */
function runSpecialHandlers() {
for (const handler of specialHandlers) {
try {
if (handler.test()) {
handler.init();
}
} catch (err) {
console.warn(`[Watermark Remover] 站点专用逻辑 ${handler.name} 初始化失败`, err);
}
}
}
function setupZsxqHandler() {
injectZsxqCss();
const state = {
observer: null,
observedRoots: new WeakSet(),
interactionTimer: null,
};
const ensureObserver = () => {
if (state.observer) return;
state.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => processNode(node));
} else if (mutation.type === 'attributes') {
const target = mutation.target;
if (target instanceof Element && shouldClearZsxqElement(target)) {
requestAnimationFrame(() => clearZsxqWatermark(target));
}
}
}
});
state.observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'watermark', 'data-watermark', 'class', 'data-testid'],
});
};
const attachBodyObserver = () => {
if (!document.body || state.observedRoots.has(document.body)) return false;
state.observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style'],
});
state.observedRoots.add(document.body);
return true;
};
const processNode = (node) => {
if (node instanceof Element) {
if (node.shadowRoot) {
observeShadowRoot(node.shadowRoot);
scanZsxqElements(node.shadowRoot);
}
requestAnimationFrame(() => scanZsxqElements(node));
} else if (node instanceof ShadowRoot || node instanceof DocumentFragment) {
observeShadowRoot(node);
requestAnimationFrame(() => scanZsxqElements(node));
}
};
const observeShadowRoot = (root) => {
if (!state.observer || state.observedRoots.has(root)) return;
try {
state.observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'watermark', 'data-watermark', 'data-testid'],
});
state.observedRoots.add(root);
} catch (err) {
console.debug('[Watermark Remover] 无法监听 ShadowRoot:', err);
}
};
const scanZsxqElements = (root) => {
const startNode = root instanceof Document ? root.documentElement : root;
if (!startNode) return;
walkDom(startNode, (el) => {
if (shouldClearZsxqElement(el)) {
clearZsxqWatermark(el);
}
});
};
const scheduleRescanAfterInteraction = () => {
if (state.interactionTimer) clearTimeout(state.interactionTimer);
state.interactionTimer = setTimeout(() => {
state.interactionTimer = null;
if (document.body) scanZsxqElements(document.body);
}, 250);
};
const shouldClearZsxqElement = (el) => {
if (!(el instanceof Element) || shouldSkipButton(el)) return false;
if (
el.hasAttribute('watermark') ||
el.hasAttribute('data-watermark') ||
(el.classList && [...el.classList].some((cls) => /watermark/i.test(cls)))
) {
return true;
}
const testId = el.getAttribute('data-testid');
if (testId && /watermark/i.test(testId)) return true;
const inline = el.getAttribute('style');
if (
inline &&
(/(?:background|background-image)\s*:\s*url\(\s*data:image/i.test(inline) ||
/(?:mask|mask-image|webkit-mask-image)\s*:\s*url\(\s*data:image/i.test(inline))
) {
return true;
}
const computed = safeComputedStyle(el);
if (computed) {
const bg = computed.backgroundImage;
if (bg && bg.includes('data:image')) return true;
const mask = computed.maskImage || computed.webkitMaskImage;
if (mask && mask.includes('data:image')) return true;
}
return false;
};
const clearZsxqWatermark = (el) => {
if (!(el instanceof Element) || shouldSkipButton(el)) return;
const inlineStyle = el.getAttribute('style');
if (inlineStyle && /nullbackground/i.test(inlineStyle)) {
el.setAttribute('style', inlineStyle.replace(/nullbackground/gi, 'background'));
}
const inlineBgImage = el.style.getPropertyValue('background-image');
if (inlineBgImage && inlineBgImage !== 'none' && inlineBgImage.includes('url(')) {
el.style.setProperty('background-image', 'none', 'important');
}
const inlineBg = el.style.getPropertyValue('background');
if (inlineBg && inlineBg.includes('url(')) {
el.style.setProperty(
'background',
inlineBg.replace(/url\([^)]*\)/gi, 'none').trim(),
'important'
);
}
const inlineMask =
el.style.getPropertyValue('mask-image') ||
el.style.getPropertyValue('-webkit-mask-image');
if (inlineMask && inlineMask.includes('url(')) {
el.style.setProperty('mask-image', 'none', 'important');
el.style.setProperty('-webkit-mask-image', 'none', 'important');
}
const computed = safeComputedStyle(el);
if (computed) {
const computedBg = computed.backgroundImage;
if (computedBg && computedBg !== 'none' && computedBg.includes('url(')) {
el.style.setProperty('background-image', 'none', 'important');
}
const computedMask = computed.maskImage || computed.webkitMaskImage;
if (computedMask && computedMask !== 'none' && computedMask.includes('url(')) {
el.style.setProperty('mask-image', 'none', 'important');
el.style.setProperty('-webkit-mask-image', 'none', 'important');
}
}
};
ensureObserver();
whenBody(() => {
attachBodyObserver();
scanZsxqElements(document.body);
});
document.addEventListener('click', scheduleRescanAfterInteraction, true);
document.addEventListener('keydown', scheduleRescanAfterInteraction, true);
setInterval(() => {
if (document.body) scanZsxqElements(document.body);
}, 3000);
}
function injectZsxqCss() {
if (document.getElementById(ZSXQ_STYLE_ID)) return;
const style = document.createElement('style');
style.id = ZSXQ_STYLE_ID;
style.textContent = `
[watermark],
[data-watermark],
[class*="watermark" i],
[data-testid*="watermark" i] {
background-image: none !important;
mask-image: none !important;
-webkit-mask-image: none !important;
}
[watermark]::before,
[watermark]::after,
[data-watermark]::before,
[data-watermark]::after,
[class*="watermark" i]::before,
[class*="watermark" i]::after {
background-image: none !important;
mask-image: none !important;
-webkit-mask-image: none !important;
}
`;
document.head.appendChild(style);
}
/* ========== 通用高风险逻辑(按钮触发) ========== */
function highRiskSweep(root) {
let processed = 0;
walkDom(root, (el) => {
if (!(el instanceof Element) || shouldSkipButton(el)) return;
const computed = safeComputedStyle(el);
let changed = false;
if (computed) {
const bg = computed.backgroundImage;
if (bg && bg !== 'none') {
el.style.setProperty('background-image', 'none', 'important');
changed = true;
}
const mask = computed.maskImage || computed.webkitMaskImage;
if (mask && mask !== 'none') {
el.style.setProperty('mask-image', 'none', 'important');
el.style.setProperty('-webkit-mask-image', 'none', 'important');
changed = true;
}
}
const inlineBg = el.style.getPropertyValue('background');
if (inlineBg && inlineBg.includes('url(')) {
el.style.setProperty(
'background',
inlineBg.replace(/url\([^)]*\)/gi, 'none').trim(),
'important'
);
changed = true;
}
const inlineBgImage = el.style.getPropertyValue('background-image');
if (inlineBgImage && inlineBgImage !== 'none') {
el.style.setProperty('background-image', 'none', 'important');
changed = true;
}
const inlineMask =
el.style.getPropertyValue('mask-image') ||
el.style.getPropertyValue('-webkit-mask-image');
if (inlineMask && inlineMask !== 'none') {
el.style.setProperty('mask-image', 'none', 'important');
el.style.setProperty('-webkit-mask-image', 'none', 'important');
changed = true;
}
if (changed) processed++;
});
return processed;
}
/* ========== 悬浮按钮逻辑(拖动贴边 & 位置记忆) ========== */
function ensureSweepButton() {
if (sweepButton) return;
sweepButton = document.createElement('button');
sweepButton.id = BUTTON_ID;
sweepButton.type = 'button';
sweepButton.textContent = '暴力去水印';
sweepButton.title = '高风险:遍历全页面并移除所有背景 / 蒙层(包括 Shadow DOM)';
applyButtonPlacement(buttonPos);
sweepButton.addEventListener('pointerdown', onPointerDown);
sweepButton.addEventListener('pointermove', onPointerMove);
sweepButton.addEventListener('pointerup', onPointerUp);
sweepButton.addEventListener('pointercancel', onPointerCancel);
sweepButton.addEventListener('click', (event) => {
if (suppressClick) {
event.stopPropagation();
event.preventDefault();
return;
}
const root = document.body || document.documentElement;
const start = performance.now();
const count = highRiskSweep(root);
const duration = (performance.now() - start).toFixed(1);
console.info(`[Watermark Remover] 高风险清理:处理了 ${count} 个元素,用时 ${duration}ms`);
});
document.body.appendChild(sweepButton);
}
function onPointerDown(event) {
if (!sweepButton) return;
dragState.active = true;
dragState.moved = false;
dragState.pointerId = event.pointerId;
dragState.startX = event.clientX;
dragState.startY = event.clientY;
try {
sweepButton.setPointerCapture(event.pointerId);
} catch (_) {}
}
function onPointerMove(event) {
if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;
const dx = event.clientX - dragState.startX;
const dy = event.clientY - dragState.startY;
if (!dragState.moved) {
if (Math.hypot(dx, dy) > 4) {
dragState.moved = true;
sweepButton.classList.add('dragging');
} else {
return;
}
}
event.preventDefault();
const side = event.clientX >= window.innerWidth / 2 ? 'right' : 'left';
applyButtonSide(side);
const topRatio = clamp(event.clientY / window.innerHeight, 0.05, 0.95);
applyButtonTop(topRatio);
buttonPos = { side, top: topRatio };
}
function onPointerUp(event) {
if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;
try {
sweepButton.releasePointerCapture(event.pointerId);
} catch (_) {}
if (dragState.moved) {
event.preventDefault();
saveButtonPosition(buttonPos);
suppressClick = true;
setTimeout(() => {
suppressClick = false;
}, 0);
}
sweepButton.classList.remove('dragging');
dragState.active = false;
dragState.moved = false;
dragState.pointerId = null;
}
function onPointerCancel(event) {
if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;
try {
sweepButton.releasePointerCapture(event.pointerId);
} catch (_) {}
sweepButton.classList.remove('dragging');
dragState.active = false;
dragState.moved = false;
dragState.pointerId = null;
}
function applyButtonPlacement(pos) {
applyButtonSide(pos.side);
applyButtonTop(pos.top);
}
function applyButtonSide(side) {
if (!sweepButton) return;
if (side === 'right') {
sweepButton.classList.add('side-right');
sweepButton.classList.remove('side-left');
sweepButton.style.left = 'auto';
sweepButton.style.right = '0';
} else {
sweepButton.classList.add('side-left');
sweepButton.classList.remove('side-right');
sweepButton.style.left = '0';
sweepButton.style.right = 'auto';
}
buttonPos.side = side === 'right' ? 'right' : 'left';
}
function applyButtonTop(topRatio) {
if (!sweepButton) return;
const clamped = clamp(topRatio, 0.05, 0.95);
sweepButton.style.top = (clamped * 100).toFixed(2) + 'vh';
buttonPos.top = clamped;
}
function injectBaseCss() {
if (document.getElementById(BASE_STYLE_ID)) return;
const style = document.createElement('style');
style.id = BASE_STYLE_ID;
style.textContent = `
#${BUTTON_ID} {
position: fixed;
top: 50%;
left: 0;
transform: translate(-88%, -50%);
padding: 11px 24px;
border: none;
border-radius: 0 18px 18px 0;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.08em;
color: #ffffff;
background: linear-gradient(135deg, #1d5fd7 0%, #0f3eb7 50%, #0d2a8e 100%);
box-shadow: 0 16px 32px rgba(9, 40, 90, 0.45);
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.35);
cursor: grab;
z-index: 2147483646;
opacity: 0.96;
transition: transform 0.25s ease, opacity 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
touch-action: none;
user-select: none;
}
#${BUTTON_ID}.side-right {
left: auto;
right: 0;
border-radius: 18px 0 0 18px;
transform: translate(88%, -50%);
}
#${BUTTON_ID}.side-left:hover,
#${BUTTON_ID}.side-left:focus-visible,
#${BUTTON_ID}.side-left.dragging,
#${BUTTON_ID}.side-right:hover,
#${BUTTON_ID}.side-right:focus-visible,
#${BUTTON_ID}.side-right.dragging {
transform: translate(0, -50%);
opacity: 1;
box-shadow: 0 20px 36px rgba(9, 40, 90, 0.55);
}
#${BUTTON_ID}:active {
background: linear-gradient(135deg, #184fc0 0%, #0c3296 100%);
box-shadow: 0 12px 28px rgba(9, 40, 90, 0.5);
cursor: grabbing;
}
#${BUTTON_ID}.dragging {
transition: none;
}
#${BUTTON_ID}:focus,
#${BUTTON_ID}:focus-visible {
outline: none;
}
#${BUTTON_ID}::after {
content: '⟲';
margin-left: 10px;
font-size: 14px;
text-shadow: inherit;
}
`;
document.head.appendChild(style);
}
/* ========== 状态工具 ========== */
function loadButtonPosition() {
const fallback = { side: 'left', top: 0.5 };
try {
if (typeof GM_getValue === 'function') {
const stored = GM_getValue(BUTTON_STORAGE_KEY);
if (stored && typeof stored === 'object') {
return normalizeButtonPos(stored, fallback);
}
} else if (window.localStorage) {
const raw = window.localStorage.getItem(BUTTON_STORAGE_KEY);
if (raw) {
return normalizeButtonPos(JSON.parse(raw), fallback);
}
}
} catch (err) {
console.debug('[Watermark Remover] 读取按钮位置失败:', err);
}
return { ...fallback };
}
function saveButtonPosition(pos) {
const normalized = normalizeButtonPos(pos, { side: 'left', top: 0.5 });
try {
if (typeof GM_setValue === 'function') {
GM_setValue(BUTTON_STORAGE_KEY, normalized);
} else if (window.localStorage) {
window.localStorage.setItem(BUTTON_STORAGE_KEY, JSON.stringify(normalized));
}
} catch (err) {
console.debug('[Watermark Remover] 保存按钮位置失败:', err);
}
}
function normalizeButtonPos(pos, fallback) {
if (!pos || typeof pos !== 'object') return { ...fallback };
const side = pos.side === 'right' ? 'right' : 'left';
const top = clamp(typeof pos.top === 'number' ? pos.top : fallback.top, 0.05, 0.95);
return { side, top };
}
/* ========== 辅助函数 ========== */
function walkDom(root, cb) {
if (!root) return;
if (root instanceof Element) {
cb(root);
if (root.shadowRoot) walkDom(root.shadowRoot, cb);
for (const child of root.children) {
walkDom(child, cb);
}
} else if (
root instanceof DocumentFragment ||
root instanceof ShadowRoot ||
root instanceof Document
) {
const nodes = root.children || root.childNodes;
for (const child of nodes) {
if (child.nodeType === 1) walkDom(child, cb);
}
}
}
function shouldSkipButton(el) {
return sweepButton && sweepButton.contains(el);
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function whenReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
} else {
fn();
}
}
function whenBody(fn) {
if (document.body) {
fn();
} else {
const watcher = new MutationObserver(() => {
if (document.body) {
watcher.disconnect();
fn();
}
});
watcher.observe(document.documentElement, { childList: true });
}
}
function safeComputedStyle(el) {
try {
return window.getComputedStyle(el);
} catch {
return null;
}
}
})();