悬停预览

提升网页浏览效率,一键悬停即现浏览器原生小窗预览,实现200毫秒快速预读,小窗智能复用,资源利用最大化。体验高效浏览,从这里开始。

As of 2024-09-02. See the latest version.

// ==UserScript==
// @name         悬停预览
// @version      3.1
// @description  提升网页浏览效率,一键悬停即现浏览器原生小窗预览,实现200毫秒快速预读,小窗智能复用,资源利用最大化。体验高效浏览,从这里开始。
// @author       hiisme
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @namespace https://greasyfork.org/users/217852
// ==/UserScript==

(function () {
    'use strict';

    let hoverTimeoutId = null;
    let prefetchTimeoutId = null;
    let popupWindow = null;
    let popupWindowRect = null;
    let isMouseOverPopup = false;

    // 读取设置或设置默认值
    const hoverDelay = GM_getValue('hoverDelay', 1200);
    const windowWidth = GM_getValue('windowWidth', 690);
    const windowHeight = GM_getValue('windowHeight', 400);

    // 注册菜单命令以更改设置
    GM_registerMenuCommand('设置悬停延迟时间 (毫秒)', () => {
        const delay = prompt('请输入悬停延迟时间(毫秒):', hoverDelay);
        if (delay !== null) {
            const parsedDelay = parseInt(delay, 10) || 1200;
            GM_setValue('hoverDelay', parsedDelay);
            alert(`悬停延迟时间设置为 ${parsedDelay} 毫秒。`);
        }
    });

    GM_registerMenuCommand('设置小窗大小', () => {
        const width = prompt('请输入窗口宽度:', windowWidth);
        const height = prompt('请输入窗口高度:', windowHeight);
        if (width !== null && height !== null) {
            const parsedWidth = parseInt(width, 10) || 690;
            const parsedHeight = parseInt(height, 10) || 400;
            GM_setValue('windowWidth', parsedWidth);
            GM_setValue('windowHeight', parsedHeight);
            alert(`窗口大小设置为 ${parsedWidth}x${parsedHeight}。`);
        }
    });

    // 预取链接
    const prefetchLink = async (url) => {
        // 清除之前的预取链接
        clearTimeout(prefetchTimeoutId);

        // 删除之前同样链接的预取
        document.querySelectorAll(`.tm-prefetch[href="${url}"]`).forEach(link => link.remove());

        return new Promise((resolve) => {
            const linkElement = document.createElement('link');
            linkElement.rel = 'prefetch';
            linkElement.href = url;
            linkElement.className = 'tm-prefetch';
            linkElement.onload = () => resolve(true);
            linkElement.onerror = () => resolve(false);
            document.head.appendChild(linkElement);
        });
    };

    // 创建或更新小窗
    const createOrUpdatePopupWindow = async (url, x, y) => {
        if (popupWindow && !popupWindow.closed) {
            if (popupWindow.location.href !== url) {
                popupWindow.location.href = url;
            }
            popupWindow.moveTo(x, y);
        } else {
            popupWindow = window.open(url, 'popupWindow', `width=${windowWidth},height=${windowHeight},top=${y},left=${x},scrollbars=yes,resizable=yes`);
        }

        if (popupWindow) {
            await new Promise((resolve) => {
                popupWindow.addEventListener('load', () => {
                    // 计算小窗的位置和大小
                    popupWindowRect = {
                        left: popupWindow.screenX,
                        top: popupWindow.screenY,
                        right: popupWindow.screenX + popupWindow.innerWidth,
                        bottom: popupWindow.screenY + popupWindow.innerHeight
                    };
                    resolve();
                });
            });

            // 确保当鼠标进入小窗时不关闭它
            popupWindow.addEventListener('focus', () => {
                isMouseOverPopup = true;
            });

            popupWindow.addEventListener('blur', () => {
                isMouseOverPopup = false;
                closePopupWindow();
            });
        }
    };

    // 关闭小窗
    const closePopupWindow = () => {
        if (popupWindow && !popupWindow.closed && !isMouseOverPopup) {
            // 延迟关闭以确认鼠标真的在外面
            setTimeout(() => {
                if (!isMouseOverPopup) {
                    popupWindow.close();
                    popupWindow = null;
                    popupWindowRect = null;
                }
            }, 200); // 延迟时间,确保鼠标不会快速返回
        }
    };

    // 防抖动函数
    const debounce = (fn, delay) => {
        let timeoutId;
        return (...args) => {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(() => fn.apply(this, args), delay);
        };
    };

    // 处理鼠标悬停事件
    const handleMouseOver = async (event) => {
        // 检查事件是否发生在主窗口中
        if (window.name === 'popupWindow') return; // 防止在小窗中触发悬停行为

        const linkElement = event.target.closest('a');
        if (linkElement && linkElement.href) {
            clearTimeout(hoverTimeoutId);
            clearTimeout(prefetchTimeoutId);

            // 200ms 后预取链接
            prefetchTimeoutId = setTimeout(() => prefetchLink(linkElement.href), 200);

            hoverTimeoutId = setTimeout(async () => {
                const { clientX: x, clientY: y } = event;
                await createOrUpdatePopupWindow(linkElement.href, x + 10, y + 10);
            }, hoverDelay);
        }
    };

    // 处理鼠标移出事件
    const handleMouseOut = (event) => {
        clearTimeout(hoverTimeoutId);
        clearTimeout(prefetchTimeoutId);

        // 移除预取链接
        document.querySelectorAll('.tm-prefetch').forEach(link => link.remove());

        if (popupWindow && !popupWindow.closed && popupWindowRect && !isMouseOverPopup) {
            const { clientX: x, clientY: y } = event;
            const outsidePopupWindow = (
                x < popupWindowRect.left ||
                x > popupWindowRect.right ||
                y < popupWindowRect.top ||
                y > popupWindowRect.bottom
            );

            if (outsidePopupWindow) {
                closePopupWindow();
            }
        }
    };

    // 处理窗口聚焦事件
    const handleWindowFocus = () => {
        closePopupWindow();
    };

    // 处理滚动和点击事件
    const handleDocumentScrollOrClick = debounce(closePopupWindow, 100);

    // 清理资源和事件监听器
    const cleanup = () => {
        clearTimeout(hoverTimeoutId);
        clearTimeout(prefetchTimeoutId);

        document.querySelectorAll('.tm-prefetch').forEach(link => link.remove());

        document.removeEventListener('mouseover', handleMouseOver, true);
        document.removeEventListener('mouseout', handleMouseOut, true);
        window.removeEventListener('focus', handleWindowFocus);
        document.removeEventListener('scroll', handleDocumentScrollOrClick, true);
        document.removeEventListener('click', handleDocumentScrollOrClick, true);

        closePopupWindow();
    };

    // 注册事件监听器
    document.addEventListener('mouseover', handleMouseOver, true);
    document.addEventListener('mouseout', handleMouseOut, true);
    window.addEventListener('focus', handleWindowFocus);
    document.addEventListener('scroll', handleDocumentScrollOrClick, true);
    document.addEventListener('click', handleDocumentScrollOrClick, true);

    // 页面卸载时清理
    window.addEventListener('beforeunload', cleanup);

})();