Greasy Fork

Greasy Fork is available in English.

打开网页:新标签页2 (防抖容错版)

智能识别+可视化指示器。优化交互:支持"点击-再点击"极速流,新增鼠标移出防抖机制,容错率更高。

当前为 2025-12-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         打开网页:新标签页2 (防抖容错版)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  智能识别+可视化指示器。优化交互:支持"点击-再点击"极速流,新增鼠标移出防抖机制,容错率更高。
// @author       HAZE
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // === UI 配置 (Zen Mode) ===
    const AUTO_CLOSE_TIMEOUT = 3500;   // 全局自动关闭时间(稍微延长以配合容错)
    const MOUSE_LEAVE_DELAY = 800;     // 关键修改:鼠标移出后的容错时间 (0.8秒)
    
    // 颜色配置
    const COLOR_PRIMARY = '#007AFF'; // macOS Blue
    const COLOR_SECONDARY = '#555555';
    const COLOR_POPUP_BG = 'rgba(255, 255, 255, 0.90)'; // 稍微增加一点不透明度,提升可见性
    
    // 指示器颜色
    const IND_COLOR_POPUP = '#af52de'; // macOS Purple
    const IND_COLOR_NEWTAB = '#34c759'; // macOS Green

    // === 状态管理 ===
    const getCurrentMode = () => GM_getValue('openMode', 'popup');
    const getBackgroundMode = () => GM_getValue('backgroundMode', false);
    const getIndicatorState = () => GM_getValue('showIndicator', true);
    const getCurrentDomain = () => window.location.hostname;
    const getExcludedSites = () => GM_getValue('excludedSites', []);
    const isCurrentSiteExcluded = () => getExcludedSites().includes(getCurrentDomain());

    // === CSS 注入 ===
    const style = document.createElement('style');
    style.textContent = `
        /* 指示器样式 */
        a[data-haze-link="active"] { position: relative; }
        a[data-haze-link="active"]::after {
            content: ""; display: inline-block; width: 5px; height: 5px;
            margin-left: 3px; border-radius: 50%; vertical-align: middle;
            opacity: 0.5; transition: all 0.2s; pointer-events: none;
        }
        a[data-haze-link="active"]:hover::after { transform: scale(1.5); opacity: 1; }
        .haze-ind-popup::after { background-color: ${IND_COLOR_POPUP}; box-shadow: 0 0 6px ${IND_COLOR_POPUP}; }
        .haze-ind-newtab::after { background-color: ${IND_COLOR_NEWTAB}; box-shadow: 0 0 6px ${IND_COLOR_NEWTAB}; }

        /* 弹窗动画 */
        @keyframes haze-pop-in {
            0% { opacity: 0; transform: translate(-65%, -45%) scale(0.95); }
            100% { opacity: 1; transform: translate(-65%, -50%) scale(1); }
        }
    `;
    document.head.appendChild(style);

    // === 智能链接识别 ===
    const isFunctionalLink = (link) => {
        const href = link.getAttribute('href');
        if (!href || href === '' || href === '#' || href === 'javascript:;' || href.includes('javascript:void')) return true;
        if (href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('sms:')) return true;
        if (link.target === '_self' || link.target === '_top' || link.target === 'iframe') return true;
        
        const role = link.getAttribute('role');
        if (role && ['button', 'tab', 'menuitem', 'option', 'switch', 'checkbox', 'radio', 'treeitem'].includes(role)) return true;

        try {
            if (href.startsWith('#')) return true;
            const urlObj = new URL(link.href);
            if (urlObj.pathname === window.location.pathname && urlObj.hash !== '') return true;
        } catch (e) {}

        const functionalityAttrs = [
            'onclick', 'download', 'data-toggle', 'data-trigger', 'data-target', 'data-action', 'data-dismiss', 'data-cmd',
            'aria-controls', 'aria-expanded', 'aria-haspopup', 'aria-disabled', 'aria-selected',
            'ng-click', '@click', 'v-on:click', ':click', 'hx-get', 'hx-post'
        ];
        for (const attr of functionalityAttrs) {
            if (link.hasAttribute(attr)) return true;
        }

        const text = link.textContent.trim();
        if (/^\d+$/.test(text)) return true;

        const className = (link.className || '').toLowerCase();
        if (className.includes('script-link')) return false;

        const strToCheck = (
            className + ' ' + (link.id || '') + ' ' + (link.title || '') + ' ' +
            (link.getAttribute('aria-label') || '') + ' ' + (link.parentElement ? link.parentElement.className : '')
        ).toLowerCase();

        const functionalKeywords = [
            'login', 'logout', 'signin', 'register', 'submit', 'cancel', 'edit', 'delete', 'setting', 'close', 
            'expand', 'collapse', 'load more', 'next', 'prev', 'filter', 'sort', 'search', 'cart', 'buy', 
            'sku', 'select', 'like', 'fav', 'share', 'reply', 'comment', 'play', 'pause'
        ];
        if (functionalKeywords.some(kw => strToCheck.includes(kw))) return true;

        const cnKeywords = ['登录', '注册', '注销', '提交', '取消', '编辑', '删除', '设置', '更多', '展开', '收起', '筛选', '排序', '购物车', '购买', '点赞', '收藏', '分享', '回复', '评论', '播放'];
        const lowerText = text.toLowerCase();
        if (lowerText.length <= 5 && cnKeywords.some(kw => lowerText.includes(kw))) return true;
        if (cnKeywords.some(kw => strToCheck.includes(kw))) return true;

        return false;
    };

    const isSystemFolderLink = (href) => /^file:\/\/\/[a-zA-Z]:\//.test(href);

    // === 指示器逻辑 ===
    const updateLinkIndicators = () => {
        if (!getIndicatorState() || isCurrentSiteExcluded() || getCurrentMode() === 'default') {
            document.querySelectorAll('a[data-haze-link]').forEach(el => {
                el.removeAttribute('data-haze-link');
                el.className = el.className.replace(/haze-ind-\w+/g, '').trim();
            });
            return;
        }
        const mode = getCurrentMode();
        const indicatorClass = mode === 'popup' ? 'haze-ind-popup' : 'haze-ind-newtab';
        
        document.querySelectorAll('a:not([data-haze-link])').forEach(link => {
            if (!isFunctionalLink(link) && !isSystemFolderLink(link.href)) {
                link.setAttribute('data-haze-link', 'active');
                link.classList.add(indicatorClass);
            }
        });
    };

    const observeDOM = () => {
        const observer = new MutationObserver((mutations) => {
            if (mutations.some(m => m.type === 'childList' && m.addedNodes.length > 0)) {
                setTimeout(updateLinkIndicators, 500);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    };

    // === UI 渲染 (关键修改:防抖机制) ===
    const createLinkOptionsPopup = (event, link) => {
        if (isCurrentSiteExcluded() || isFunctionalLink(link) || isSystemFolderLink(link.href)) return;

        // 移除已存在的弹窗(防止多开)
        const existing = document.getElementById('haze-popup');
        if (existing) existing.remove();

        const popup = document.createElement('div');
        popup.id = 'haze-popup';
        
        Object.assign(popup.style, {
            position: 'fixed',
            top: `${event.clientY}px`,
            left: `${event.clientX}px`,
            transform: 'translate(-65%, -50%)', // 保持偏移,确保右侧按钮在鼠标下
            backgroundColor: COLOR_POPUP_BG,
            backdropFilter: 'blur(12px)',
            webkitBackdropFilter: 'blur(12px)',
            borderRadius: '10px',
            boxShadow: '0 8px 32px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.05)',
            border: '1px solid rgba(255,255,255,0.4)',
            padding: '5px',
            zIndex: '2147483647',
            display: 'flex',
            gap: '6px',
            pointerEvents: 'auto',
            animation: 'haze-pop-in 0.15s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards',
            boxSizing: 'border-box'
        });

        const createBtn = (text, isPrimary) => {
            const btn = document.createElement('div');
            btn.textContent = text;
            Object.assign(btn.style, {
                padding: '6px 12px',
                borderRadius: '6px',
                cursor: 'pointer',
                fontSize: '13px',
                fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
                fontWeight: isPrimary ? '600' : '400',
                color: isPrimary ? COLOR_PRIMARY : COLOR_SECONDARY,
                backgroundColor: isPrimary ? 'rgba(0, 122, 255, 0.1)' : 'transparent',
                transition: 'all 0.1s ease',
                whiteSpace: 'nowrap',
                userSelect: 'none',
                textAlign: 'center',
                minWidth: isPrimary ? '80px' : '60px',
                flex: isPrimary ? '1.5' : '1'
            });

            btn.addEventListener('mouseenter', () => {
                btn.style.backgroundColor = isPrimary ? 'rgba(0, 122, 255, 0.2)' : 'rgba(0,0,0,0.05)';
                btn.style.transform = 'translateY(-1px)';
            });
            btn.addEventListener('mouseleave', () => {
                btn.style.backgroundColor = isPrimary ? 'rgba(0, 122, 255, 0.1)' : 'transparent';
                btn.style.transform = 'translateY(0)';
            });
            
            return btn;
        };

        const btnCurrent = createBtn('🏠 当前', false);
        const isBg = getBackgroundMode();
        const btnNewTab = createBtn(isBg ? '🚀 后台' : '↗ 新标签', true);

        btnCurrent.onclick = (e) => {
            e.stopPropagation();
            window.location.href = link.href;
            popup.remove();
        };

        btnNewTab.onclick = (e) => {
            e.stopPropagation();
            if (getBackgroundMode()) {
                GM_openInTab(link.href, { active: false, insert: true, setParent: true });
            } else {
                window.open(link.href, '_blank');
            }
            popup.remove();
        };

        popup.appendChild(btnCurrent);
        popup.appendChild(btnNewTab);
        document.body.appendChild(popup);

        // === 智能防抖关闭逻辑 (关键) ===
        let autoCloseTimer = setTimeout(() => { if (popup.parentNode) popup.remove(); }, AUTO_CLOSE_TIMEOUT);
        let leaveTimer = null;

        popup.addEventListener('mouseenter', () => {
            // 鼠标进入,清除所有关闭倒计时
            clearTimeout(autoCloseTimer);
            if (leaveTimer) clearTimeout(leaveTimer);
        });

        popup.addEventListener('mouseleave', () => {
            // 鼠标离开,启动容错倒计时 (800ms)
            // 如果用户只是手滑并在800ms内移回,这里会被 mouseenter 的 clear 清除,从而不关闭
            leaveTimer = setTimeout(() => { 
                if (popup.parentNode) popup.remove(); 
            }, MOUSE_LEAVE_DELAY);
        });
    };

    // === 事件监听 ===
    const handleLinkClick = (event) => {
        if (isCurrentSiteExcluded()) return;
        const link = event.target.closest('a');
        if (!link || !link.href) return;
        
        if (event.ctrlKey || event.metaKey || event.shiftKey) return;
        if (isFunctionalLink(link) || isSystemFolderLink(link.href)) return;

        const currentMode = getCurrentMode();
        if (currentMode === 'popup') {
            event.preventDefault();
            event.stopPropagation();
            createLinkOptionsPopup(event, link);
        } else if (currentMode === 'newtab') {
            event.preventDefault();
            event.stopPropagation();
            if (getBackgroundMode()) {
                GM_openInTab(link.href, { active: false, insert: true, setParent: true });
            } else {
                window.open(link.href, '_blank');
            }
        }
    };

    // === 菜单注册 ===
    let menuIds = [];
    const registerMenu = () => {
        menuIds.forEach(id => GM_unregisterMenuCommand(id));
        menuIds = [];

        const mode = getCurrentMode();
        const isBg = getBackgroundMode();
        const showInd = getIndicatorState();
        
        const modeText = {
            'popup': '🟣 模式:选择框 (当前)',
            'newtab': '🟢 模式:直接新标签',
            'default': '⚪ 模式:浏览器默认'
        };

        menuIds.push(GM_registerMenuCommand(modeText[mode], () => {
            const next = mode === 'popup' ? 'newtab' : (mode === 'newtab' ? 'default' : 'popup');
            GM_setValue('openMode', next);
            location.reload();
        }));

        if (mode !== 'default') {
            menuIds.push(GM_registerMenuCommand(isBg ? '⚙️ 新标签:后台静默' : '⚙️ 新标签:前台跳转', () => {
                GM_setValue('backgroundMode', !isBg);
                registerMenu();
            }));
        }

        menuIds.push(GM_registerMenuCommand(showInd ? '👁️ 指示器:开启' : '👁️ 指示器:关闭', () => {
            GM_setValue('showIndicator', !showInd);
            location.reload();
        }));

        if (isCurrentSiteExcluded()) {
            menuIds.push(GM_registerMenuCommand(`✅ 恢复此网站`, () => {
                const s = getExcludedSites();
                s.splice(s.indexOf(getCurrentDomain()), 1);
                GM_setValue('excludedSites', s);
                location.reload();
            }));
        } else {
            menuIds.push(GM_registerMenuCommand(`🚫 排除此网站`, () => {
                const s = getExcludedSites();
                s.push(getCurrentDomain());
                GM_setValue('excludedSites', s);
                location.reload();
            }));
        }
    };

    const init = () => {
        document.addEventListener('click', handleLinkClick, true);
        registerMenu();
        updateLinkIndicators();
        observeDOM();
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();