Greasy Fork

Greasy Fork is available in English.

控制页面的图片显示与隐藏按钮

1. 修复翻页(上一页/下一页)隐藏失效 2. 恢复 Version 12 的滤镜动画 3. 修复虚线框随父级折叠 4. 解决按钮穿透置顶 5. 优化动态加载兼容性 6. 新增单页应用(SPA)路由劫持与懒加载监听 7. 解决折叠裁剪失效及层级置顶穿透 8. 修复知乎等站点虚线框偏移问题

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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));
    });

})();