Greasy Fork is available in English.
1. 修复翻页(上一页/下一页)隐藏失效 2. 恢复 Version 12 的滤镜动画 3. 修复虚线框随父级折叠 4. 解决按钮穿透置顶 5. 优化动态加载兼容性 6. 新增单页应用(SPA)路由劫持与懒加载监听 7. 解决折叠裁剪失效及层级置顶穿透 8. 修复知乎等站点虚线框偏移问题
// ==UserScript==
// @name 控制页面的图片显示与隐藏按钮
// @version 24
// @description 1. 修复翻页(上一页/下一页)隐藏失效 2. 恢复 Version 12 的滤镜动画 3. 修复虚线框随父级折叠 4. 解决按钮穿透置顶 5. 优化动态加载兼容性 6. 新增单页应用(SPA)路由劫持与懒加载监听 7. 解决折叠裁剪失效及层级置顶穿透 8. 修复知乎等站点虚线框偏移问题
// @author fxalll
// @match *://*/*
// @grant none
// @run-at document-body
// @license MIT
// @namespace http://greasyfork.icu/users/1043548
// ==/UserScript==
(function () {
let startTime = 0;
let initialY, yOffset = 0;
let isDragging = false;
let showOutlineConfig = localStorage.getItem('nopicShowOutline') !== 'false';
let hoverOnlyConfig = localStorage.getItem('nopicHoverOnly') === 'true';
let hoverShowImgConfig = localStorage.getItem('nopicHoverShowImg') === 'true';
window.imgHidenSet = null;
let imageControls = new Map();
let imageOutlines = new Map();
// --- 1. 注入增强样式 ---
const style = document.createElement('style');
style.id = 'nopic-injected-styles';
style.innerHTML = `
img, svg, .nopic-has-bg {
transition: filter 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease !important;
}
.nopic-hidden {
filter: blur(25px) !important;
opacity: 0 !important;
pointer-events: none !important;
}
.nopic-ui-reset {
box-sizing: border-box !important;
margin: 0 !important;
line-height: 1 !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
}
.nopic-outline-box {
position: absolute !important;
z-index: 10;
pointer-events: none;
box-sizing: border-box;
border-radius: 4px;
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1),
background-position 0.5s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0;
background-image:
linear-gradient(90deg, #919191 50%, transparent 50%),
linear-gradient(90deg, #919191 50%, transparent 50%),
linear-gradient(0deg, #919191 50%, transparent 50%),
linear-gradient(0deg, #919191 50%, transparent 50%);
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
background-size: 15px 2px, 15px 2px, 2px 15px, 2px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}
.nopic-outline-active {
opacity: 1 !important;
background-position: 30px 0, -30px 100%, 0 -30px, 100% 30px !important;
}
.nopic-float-btn {
display: flex; align-items: center; justify-content: center;
position: absolute !important; z-index: 11;
background: #4f4f4f; color: #fff;
cursor: pointer; border-radius: 6px; user-select: none;
backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.4);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
opacity: 0; transform: scale(0.7); pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease, background 0.2s;
}
.nopic-float-btn:hover { background: #2f2f2f !important; }
.nopic-btn-active { opacity: 1 !important; transform: scale(1) !important; pointer-events: auto !important; }
.nopic-side-panel { z-index: 2147483647 !important; }
.nopic-menu-item {
padding: 0 12px; height: 32px; margin: 0 4px; font-size: 12px;
background: rgba(255,255,255,0.06); border-radius: 10px; border: 1px solid rgba(255,255,255,0.1);
transition: all 0.2s; cursor:pointer; user-select:none; display:flex; align-items:center;
}
`;
document.head.appendChild(style);
// --- 2. 核心位置与状态同步 ---
const syncElementPosition = (el) => {
const btn = imageControls.get(el);
const outline = imageOutlines.get(el);
if (!el || !el.isConnected) {
btn?.remove(); outline?.remove();
imageControls.delete(el); imageOutlines.delete(el);
return;
}
let top = el.offsetTop;
let left = el.offsetLeft;
const width = el.offsetWidth;
const height = el.offsetHeight;
// 【修复知乎等站点虚线框偏移】使用 BoundingClientRect 计算精确相对坐标
// 从而忽略 inline 包装、margin 塌陷导致的 offsetParent 计算偏差
if (outline && outline.parentElement) {
const parent = outline.parentElement;
const imgRect = el.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
const pStyle = window.getComputedStyle(parent);
const borderTop = parseFloat(pStyle.borderTopWidth) || 0;
const borderLeft = parseFloat(pStyle.borderLeftWidth) || 0;
top = imgRect.top - parentRect.top + parent.scrollTop - borderTop;
left = imgRect.left - parentRect.left + parent.scrollLeft - borderLeft;
}
if (width <= 0 || height <= 0) {
if(btn) btn.style.display = 'none';
if(outline) outline.style.display = 'none';
return;
} else {
if(btn) btn.style.display = 'flex';
if(outline) outline.style.display = 'block';
}
if (btn) {
btn.style.left = (left + 6) + 'px';
btn.style.top = (top + 6) + 'px';
}
if (outline) {
outline.style.left = left + 'px';
outline.style.top = top + 'px';
outline.style.width = width + 'px';
outline.style.height = height + 'px';
const isHidden = el.dataset.isHidden === 'true';
if (isHidden) {
if (hoverShowImgConfig && el.isHovering) {
el.classList.remove('nopic-hidden');
} else {
if (!el.classList.contains('nopic-hidden')) el.classList.add('nopic-hidden');
}
} else {
el.classList.remove('nopic-hidden');
}
let shouldBeVisible = (isHidden && showOutlineConfig && (!hoverOnlyConfig || el.isHovering));
if (hoverShowImgConfig && el.isHovering) shouldBeVisible = false;
outline.classList.toggle('nopic-outline-active', !!shouldBeVisible);
btn.classList.toggle('nopic-btn-active', !!el.isHovering && !hoverShowImgConfig);
}
};
document.addEventListener('mousemove', (e) => {
if (window.imgHidenSet === null) return;
imageControls.forEach((btn, el) => {
const rect = el.getBoundingClientRect();
const isInside = (
e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom
);
if (isInside !== el.isHovering) {
el.isHovering = isInside;
syncElementPosition(el);
}
});
});
let createControlButton = function(el) {
if (imageControls.has(el)) return;
let imgStyle = window.getComputedStyle(el);
let parent;
// 关键改动:优先使用直接父级挂载,使其继承父级的折叠(overflow:hidden)状态。
// 如果图片是绝对定位,则回退到 offsetParent 以防破坏页面原有绝对定位布局。
if (imgStyle.position !== 'absolute' && imgStyle.position !== 'fixed') {
parent = el.parentElement || document.body;
} else {
parent = el.offsetParent || document.body;
}
if (window.getComputedStyle(parent).position === 'static') {
parent.style.position = 'relative';
}
const rect = el.getBoundingClientRect();
const baseSize = Math.max(20, Math.min(32, Math.min(rect.width, rect.height) * 0.4));
// 关键改动:动态获取并覆盖原先暴力的 z-index,解决置顶穿透问题
let imgZ = imgStyle.zIndex;
let targetZ = (imgZ !== 'auto' && !isNaN(imgZ)) ? parseInt(imgZ) : 1;
let outline = document.createElement('div');
outline.className = 'nopic-outline-box nopic-ui-reset';
outline.style.zIndex = targetZ; // 覆盖默认的高 z-index
parent.appendChild(outline);
imageOutlines.set(el, outline);
let button = document.createElement('div');
button.className = 'nopic-ui-reset nopic-float-btn';
button.innerText = '显';
button.style.width = (baseSize * 1.2) + 'px';
button.style.height = baseSize + 'px';
button.style.fontSize = Math.max(11, baseSize * 0.5) + 'px';
button.style.zIndex = targetZ + 1; // 覆盖默认的高 z-index,比虚线略高一层
button.addEventListener('click', (e) => {
e.stopPropagation(); e.preventDefault();
const isCurrentlyHidden = el.dataset.isHidden === 'true';
el.dataset.isHidden = isCurrentlyHidden ? 'false' : 'true';
button.innerText = isCurrentlyHidden ? '隐' : '显';
syncElementPosition(el);
});
parent.appendChild(button);
imageControls.set(el, button);
el.dataset.isHidden = 'true';
if (window.getComputedStyle(el).backgroundImage !== 'none') el.classList.add('nopic-has-bg');
el.classList.add('nopic-hidden');
syncElementPosition(el);
};
let imgHiden = function() {
if (!document.getElementById('nopic-injected-styles')) document.head.appendChild(style);
if (typeof container !== 'undefined' && !document.body.contains(container)) document.body.appendChild(container);
imageControls.forEach((btn, el) => {
if (!el.isConnected) {
btn?.remove();
imageOutlines.get(el)?.remove();
imageControls.delete(el);
imageOutlines.delete(el);
} else {
syncElementPosition(el);
}
});
document.querySelectorAll('img, svg, .nopic-has-bg, [style*="background-image"]').forEach(el => {
const bg = window.getComputedStyle(el).backgroundImage;
const isTarget = el.tagName === 'IMG' || el.tagName === 'SVG' || (bg && bg !== 'none' && bg.includes('url'));
if (isTarget) {
const rect = el.getBoundingClientRect();
const hasText = (el.tagName === 'DIV' || el.tagName === 'SPAN') && el.innerText.trim().length > 0;
if (rect.width > 15 && rect.height > 15 && !hasText) {
if (!imageControls.has(el)) createControlButton(el);
}
}
});
};
let imgShown = function() {
imageControls.forEach((btn, el) => {
el.classList.remove('nopic-hidden');
el.dataset.isHidden = 'false';
btn.remove();
});
imageOutlines.forEach(otl => otl.remove());
imageControls.clear(); imageOutlines.clear();
};
// --- 3. 增强:单页应用路由劫持与异步加载监听,彻底解决翻页失效 ---
const triggerImmediateCheck = () => {
if (window.imgHidenSet !== null) {
setTimeout(imgHiden, 50); // 给 DOM 留一点渲染时间
setTimeout(imgHiden, 300); // 确保异步内容也被捕获
}
};
// 监听原生前进/后退
window.addEventListener('popstate', triggerImmediateCheck);
// 劫持 pushState (如 Vue Router, React Router)
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
triggerImmediateCheck();
};
// 劫持 replaceState
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
triggerImmediateCheck();
};
// 捕获阶段监听新图片加载完毕 (解决由于一开始 size 为 0 导致漏判的懒加载图片)
document.addEventListener('load', (e) => {
if (window.imgHidenSet !== null) {
const el = e.target;
if (el && (el.tagName === 'IMG' || el.tagName === 'SVG')) {
imgHiden();
}
}
}, true);
// --- 4. 侧边控制台 ---
const container = document.createElement('div');
container.className = 'nopic-side-panel';
container.style.cssText = `position:fixed; top:50%; left:0; display:flex; align-items:center; transform:translateY(-50%); pointer-events:none; height: 50px;`;
const mainBtn = document.createElement('div');
mainBtn.className = 'nopic-ui-reset';
mainBtn.innerText = "◀";
mainBtn.style.cssText += `color:#fff; padding:0 20px; min-width: 50px; height: 46px; background:rgba(0,0,0,0.8); border-radius:0 25px 25px 0; cursor:grab; backdrop-filter:blur(15px); user-select:none; transition: all 0.4s; pointer-events:auto; display:flex; align-items:center; justify-content:center; border: 1px solid rgba(255,255,255,0.2); border-left:none;`;
const subMenu = document.createElement('div');
subMenu.className = 'nopic-ui-reset';
subMenu.style.cssText += `background:rgba(20,20,20,0.9); padding:0 10px; border-radius:0 25px 25px 0; opacity:0; pointer-events:none; transition: all 0.4s; transform: translateX(-30px) scale(0.95); white-space:nowrap; color:white; border: 1px solid rgba(255,255,255,0.2); border-left:none; backdrop-filter:blur(15px); display:flex; align-items:center; margin-left: -1px; height: 46px;`;
const outlineToggle = document.createElement('div'); outlineToggle.className = 'nopic-ui-reset nopic-menu-item';
const hoverToggle = document.createElement('div'); hoverToggle.className = 'nopic-ui-reset nopic-menu-item';
const hoverShowImgToggle = document.createElement('div'); hoverShowImgToggle.className = 'nopic-ui-reset nopic-menu-item';
const updateMainBtnUI = () => {
const isActive = window.imgHidenSet !== null;
mainBtn.innerHTML = container.isHovered ? `图片隐藏: <span style="margin-left:8px; color:${isActive?'#4caf50':'#999'}; font-weight:bold;">${isActive?'ON':'OFF'}</span>` : "◀";
};
const updateToggleUI = () => {
outlineToggle.innerHTML = `虚线辅助: <span style="margin-left:5px; color:${showOutlineConfig?'#4caf50':'#999'};">${showOutlineConfig?'ON':'OFF'}</span>`;
hoverToggle.innerHTML = `仅悬停显示: <span style="margin-left:5px; color:${hoverOnlyConfig?'#4caf50':'#999'};">${hoverOnlyConfig?'ON':'OFF'}</span>`;
hoverShowImgToggle.innerHTML = `悬停显图: <span style="margin-left:5px; color:${hoverShowImgConfig?'#4caf50':'#999'};">${hoverShowImgConfig?'ON':'OFF'}</span>`;
};
outlineToggle.onclick = (e) => { e.stopPropagation(); showOutlineConfig = !showOutlineConfig; localStorage.setItem('nopicShowOutline', showOutlineConfig); updateToggleUI(); if (window.imgHidenSet) imgHiden(); };
hoverToggle.onclick = (e) => { e.stopPropagation(); hoverOnlyConfig = !hoverOnlyConfig; localStorage.setItem('nopicHoverOnly', hoverOnlyConfig); updateToggleUI(); if (window.imgHidenSet) imgHiden(); };
hoverShowImgToggle.onclick = (e) => { e.stopPropagation(); hoverShowImgConfig = !hoverShowImgConfig; localStorage.setItem('nopicHoverShowImg', hoverShowImgConfig); updateToggleUI(); if (window.imgHidenSet) imgHiden(); };
subMenu.appendChild(outlineToggle);
subMenu.appendChild(hoverToggle);
subMenu.appendChild(hoverShowImgToggle);
container.appendChild(mainBtn); container.appendChild(subMenu);
container.onmouseenter = () => { container.isHovered = true; updateMainBtnUI(); mainBtn.style.minWidth = "150px"; mainBtn.style.borderRadius = "0"; subMenu.style.opacity = '1'; subMenu.style.pointerEvents = 'auto'; subMenu.style.transform = 'translateX(0px) scale(1)'; };
container.onmouseleave = () => { container.isHovered = false; mainBtn.innerText = "◀"; mainBtn.style.minWidth = "50px"; mainBtn.style.borderRadius = "0 25px 25px 0"; subMenu.style.opacity = '0'; subMenu.style.pointerEvents = 'none'; subMenu.style.transform = 'translateX(-30px) scale(0.95)'; };
mainBtn.addEventListener('mousedown', (e) => {
startTime = e.timeStamp; initialY = e.clientY - yOffset; isDragging = true;
const move = (ev) => { yOffset = ev.clientY - initialY; container.style.transform = `translateY(calc(-50% + ${yOffset}px))`; };
const up = (ev) => {
isDragging = false;
if (ev.timeStamp - startTime < 200) {
if (window.imgHidenSet === null) {
imgHiden(); window.imgHidenSet = setInterval(imgHiden, 500);
let list = (localStorage.getItem('nopicValueList') || '').split(',').filter(x=>x);
if (!list.includes(location.host)) { list.push(location.host); localStorage.setItem('nopicValueList', list.join(',')); }
} else {
clearInterval(window.imgHidenSet); window.imgHidenSet = null; imgShown();
let list = (localStorage.getItem('nopicValueList') || '').split(',').filter(v => v !== location.host && v);
localStorage.setItem('nopicValueList', list.join(','));
}
updateMainBtnUI();
}
document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up);
};
document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
});
document.body.appendChild(container);
updateToggleUI();
if ((localStorage.getItem('nopicValueList') || '').split(',').includes(location.host)) {
setTimeout(() => { imgHiden(); window.imgHidenSet = setInterval(imgHiden, 500); updateMainBtnUI(); }, 50);
}
window.addEventListener('resize', () => {
if (window.imgHidenSet) imageControls.forEach((btn, el) => syncElementPosition(el));
});
})();