您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
通用低风险去水印、站点专用处理(含 ZSXQ)、以及手动高风险清理按钮(支持拖动贴边与位置记忆)
// ==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; } } })();