Greasy Fork

Greasy Fork is available in English.

小窗预览 长按 中键 拖拽

随心所欲,不离不弃。我在此地,不曾离去。我希望停留在此地,但我也想看着锅里的,吃着碗里的。

当前为 2024-09-01 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         小窗预览 长按 中键 拖拽
// @version      3.9.1
// @description  随心所欲,不离不弃。我在此地,不曾离去。我希望停留在此地,但我也想看着锅里的,吃着碗里的。
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_info
// @namespace    http://greasyfork.icu/users/217852
// ==/UserScript==

(function() {
    'use strict';

    // State to track dragging, popup status, overlay visibility, and long press
    const state = {
        isDragging: false,
        isLongPressing: false,
        linkToPreload: null,
        popupWindow: null,
        acrylicOverlay: null,
        overlayVisible: false,
        preloadElement: null,
        longPressTimeout: null,
        startPosition: { x: 0, y: 0 },
    };

    // Configuration settings with defaults
    const config = {
        windowWidth: GM_getValue('windowWidth', 870),
        windowHeight: GM_getValue('windowHeight', 530),
        blurIntensity: GM_getValue('blurIntensity', 20),
        blurEnabled: GM_getValue('blurEnabled', true),
        closeOnMouseClick: GM_getValue('closeOnMouseClick', true),
        closeOnScroll: GM_getValue('closeOnScroll', true),
        longPressEnabled: GM_getValue('longPressEnabled', true),
        dragToOpenEnabled: GM_getValue('dragToOpenEnabled', true),
        middleClickEnabled: GM_getValue('middleClickEnabled', true), // New option for middle click
        longPressDuration: GM_getValue('longPressDuration', 300), // 300ms default
        transitionDuration: 220, // 220ms fade-in/fade-out
    };

    // Website-specific configurations
    const specificSites = {
        "bilibili.com": { windowWidth: 1000, windowHeight: 565, enableLeftClickPopup: true },
        "douyin.com": { windowWidth: 1000, windowHeight: 500, enableLeftClickPopup: true },
        "x.com": { windowWidth: 1000, windowHeight: 700, enableLeftClickPopup: true },
        "youtube.com": { windowWidth: 1040, windowHeight: 700, enableLeftClickPopup: false }
    };

    // Utility to delay operations (e.g., to allow preload to complete)
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // Preload a link using a <link rel="prefetch"> element
    async function preloadLink(link) {
        try {
            removePreloadedLink();

            const preloadElement = document.createElement('link');
            preloadElement.rel = 'prefetch';
            preloadElement.href = link;
            preloadElement.as = 'document';
            preloadElement.onload = () => console.log(`Prefetch successful: ${link}`);
            preloadElement.onerror = () => console.log(`Prefetch failed: ${link}`);
            document.head.appendChild(preloadElement);
            state.preloadElement = preloadElement;

            await delay(1); // Ensure the prefetch operation starts
        } catch (error) {
            console.error('Error in prefetch operation:', error);
        }
    }

    // Remove the preloaded link element from the DOM
    function removePreloadedLink() {
        if (state.preloadElement) {
            state.preloadElement.remove();
            state.preloadElement = null;
        }
    }

    // Create an acrylic overlay with blur effect and fade-in transition
    function createAcrylicOverlay() {
        const acrylicOverlay = document.createElement('div');
        Object.assign(acrylicOverlay.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            zIndex: '9999',
            backdropFilter: config.blurEnabled ? `blur(${config.blurIntensity}px)` : 'none',
            opacity: '0',
            transition: `opacity ${config.transitionDuration}ms ease-in-out`,
        });

        if (config.closeOnMouseClick) {
            acrylicOverlay.addEventListener('click', handleAcrylicOverlayClick);
        }

        document.body.appendChild(acrylicOverlay);

        // Trigger fade-in effect
        requestAnimationFrame(() => {
            acrylicOverlay.style.opacity = '1';
            state.overlayVisible = true;
        });

        return acrylicOverlay;
    }

    // Handle click on the acrylic overlay (close popup)
    function handleAcrylicOverlayClick(event) {
        if (event.target === state.acrylicOverlay) {
            closePopupWindow();
        }
    }

    // Remove the acrylic overlay with fade-out transition
    function removeAcrylicOverlay() {
        if (state.acrylicOverlay && state.overlayVisible) {
            state.acrylicOverlay.style.opacity = '0';

            setTimeout(() => {
                if (state.acrylicOverlay) {
                    state.acrylicOverlay.remove();
                    state.acrylicOverlay = null;
                    state.overlayVisible = false;
                }
            }, config.transitionDuration);
        }
    }

    // Open a popup window centered on the screen
    function openPopupWindow(link, width = config.windowWidth, height = config.windowHeight) {
        const screenLeft = (window.screen.width - width) / 2;
        const screenTop = (window.screen.height - height) / 3;

        if (!state.popupWindow || state.popupWindow.closed) {
            state.acrylicOverlay = createAcrylicOverlay();
            state.popupWindow = window.open(link, '_blank', `width=${width},height=${height},left=${screenLeft},top=${screenTop}`);
            state.popupWindowChecker = setInterval(checkPopupWindowStatus, 200);
        }
    }

    // Close the popup window and cleanup
    function closePopupWindow() {
        if (state.popupWindow && !state.popupWindow.closed) {
            state.popupWindow.close();
            state.popupWindow = null;
            removeAcrylicOverlay();
            removePreloadedLink();
            window.removeEventListener('scroll', closePopupOnScroll);
        }
    }

    // Check if the popup window is still open; remove overlay if closed
    function checkPopupWindowStatus() {
        if (state.popupWindow && state.popupWindow.closed) {
            removeAcrylicOverlay();
            clearInterval(state.popupWindowChecker);
        }
    }

    // Close the popup window when the page is scrolled
    function closePopupOnScroll() {
        closePopupWindow();
    }

    // Check if the current site matches a specific configuration
    function getSiteConfig() {
        const hostname = window.location.hostname;
        return specificSites[Object.keys(specificSites).find(domain => hostname.includes(domain))];
    }

    // Register menu commands for the script
    function registerMenuCommand(label, action) {
        return GM_registerMenuCommand(label, () => {
            action();
            updateMenuCommands();
        });
    }

    // Toggle blur effect on/off
    function toggleBlurEffect() {
        config.blurEnabled = !config.blurEnabled;
        GM_setValue('blurEnabled', config.blurEnabled);
    }

    // Prompt user to set blur intensity
    function setBlurIntensity() {
        const intensity = prompt('输入模糊强度(0-100):', config.blurIntensity);
        if (intensity !== null) {
            config.blurIntensity = parseInt(intensity, 10);
            GM_setValue('blurIntensity', config.blurIntensity);
        }
    }

    // Toggle the option to close the popup on mouse click
    function toggleCloseOnMouseClick() {
        config.closeOnMouseClick = !config.closeOnMouseClick;
        GM_setValue('closeOnMouseClick', config.closeOnMouseClick);
    }

    // Toggle the option to close the popup on scroll
    function toggleCloseOnScroll() {
        config.closeOnScroll = !config.closeOnScroll;
        handleScrollCommand();
        GM_setValue('closeOnScroll', config.closeOnScroll);
    }

    // Toggle the option to enable or disable drag-to-open feature
    function toggleDragToOpen() {
        config.dragToOpenEnabled = !config.dragToOpenEnabled;
        GM_setValue('dragToOpenEnabled', config.dragToOpenEnabled);
    }

    // Toggle the option to enable or disable long-press-to-open feature
    function toggleLongPressOpen() {
        config.longPressEnabled = !config.longPressEnabled;
        GM_setValue('longPressEnabled', config.longPressEnabled);
    }

    // Toggle the option to enable or disable middle-click-to-open feature
    function toggleMiddleClickOpen() {
        config.middleClickEnabled = !config.middleClickEnabled;
        GM_setValue('middleClickEnabled', config.middleClickEnabled);
    }

    // Prompt user to set the long-press duration
    function setLongPressDuration() {
        const duration = prompt('输入长按时间(毫秒):', config.longPressDuration);
        if (duration !== null) {
            config.longPressDuration = parseInt(duration, 10);
            GM_setValue('longPressDuration', config.longPressDuration);
        }
    }

    // Prompt user to set window size
    function setWindowSize() {
        const size = prompt(`输入小窗口宽度x高度(例如: 870x530):`, `${config.windowWidth}x${config.windowHeight}`);
        if (size !== null) {
            const [width, height] = size.split('x').map(val => parseInt(val.trim(), 10));
            if (!isNaN(width) && !isNaN(height)) {
                config.windowWidth = width;
                config.windowHeight = height;
                GM_setValue('windowWidth', config.windowWidth);
                GM_setValue('windowHeight', config.windowHeight);
                if (state.popupWindow && !state.popupWindow.closed) {
                    state.popupWindow.resizeTo(config.windowWidth, config.windowHeight);
                }
            }
        }
    }

    // Update the menu commands when configuration changes
    function updateMenuCommands() {
        menuCommands.forEach((command) => {
            registerMenuCommand(command.label, command.action);
        });
    }

    // Menu commands for configuring the script
    const menuCommands = [
        { label: `模糊旧时刻 (${config.blurEnabled ? '开' : '关'})`, action: toggleBlurEffect },
        { label: `模糊的强弱 (${config.blurIntensity})`, action: setBlurIntensity },
        { label: `轻点关闭否 (${config.closeOnMouseClick ? '开' : '关'})`, action: toggleCloseOnMouseClick },
        { label: `滚动关闭否 (${config.closeOnScroll ? '开' : '关'})`, action: toggleCloseOnScroll },
        { label: `拖动打开否 (${config.dragToOpenEnabled ? '开' : '关'})`, action: toggleDragToOpen },
        { label: `长按打开否 (${config.longPressEnabled ? '开' : '关'})`, action: toggleLongPressOpen },
        { label: `中键打开小窗 (${config.middleClickEnabled ? '开' : '关'})`, action: toggleMiddleClickOpen }, // New menu command
        { label: `长按时间 (${config.longPressDuration}ms)`, action: setLongPressDuration },
        { label: `不变的大小 (${config.windowWidth}x${config.windowHeight})`, action: setWindowSize },
    ];

    // Initialize menu commands
    updateMenuCommands();

    // Event listeners for drag-and-drop, long-press, middle-click, and other interactions
    document.body.addEventListener('mousedown', (event) => {
        if (event.button !== 0) return;

        const linkElement = event.target.tagName === 'A' ? event.target : event.target.closest('a');
        if (!linkElement) return;

        // Track the start position for displacement detection
        state.startPosition = { x: event.clientX, y: event.clientY };

        if (config.longPressEnabled) {
            state.isLongPressing = true;
            state.longPressTimeout = setTimeout(async () => {
                if (state.isLongPressing) {
                    state.isLongPressing = false;
                    await preloadLink(linkElement.href);
                    openPopupWindow(linkElement.href);
                }
            }, config.longPressDuration);
        }
    });

    document.body.addEventListener('mouseup', (event) => {
        state.isLongPressing = false;
        clearTimeout(state.longPressTimeout);
    });

    document.body.addEventListener('mousemove', (event) => {
        if (state.isLongPressing) {
            const displacement = Math.sqrt(
                Math.pow(event.clientX - state.startPosition.x, 2) +
                Math.pow(event.clientY - state.startPosition.y, 2)
            );
            if (displacement > 5) {  // Cancel long-press if significant movement is detected
                state.isLongPressing = false;
                clearTimeout(state.longPressTimeout);
            }
        }
    });

    document.body.addEventListener('dragstart', async (event) => {
        if (!config.dragToOpenEnabled) return;

        const linkElement = event.target.tagName === 'A' ? event.target : event.target.closest('a');
        if (linkElement) {
            state.isDragging = true;
            state.linkToPreload = linkElement.href;

            await preloadLink(state.linkToPreload);

            if (config.closeOnScroll) {
                window.addEventListener('scroll', closePopupOnScroll, { once: true });
            }
        }
    });

    document.body.addEventListener('dragend', () => {
        if (state.isDragging && state.linkToPreload) {
            state.isDragging = false;
            openPopupWindow(state.linkToPreload);
            state.linkToPreload = null;
        }
    });

    document.body.addEventListener('wheel', () => {
        if (config.closeOnScroll) {
            closePopupWindow();
        }
    });

    document.body.addEventListener('click', (event) => {
        if (event.target === state.acrylicOverlay) {
            removeAcrylicOverlay();
        }
    });

    document.body.addEventListener('auxclick', async (event) => {
        if (event.button !== 1 || !config.middleClickEnabled) return; // Check for middle click and if the feature is enabled

        const linkElement = event.target.tagName === 'A' ? event.target : event.target.closest('a');
        if (!linkElement) return;

        event.preventDefault(); // Prevent the default middle click behavior

        await preloadLink(linkElement.href);
        openPopupWindow(linkElement.href);
    });

})();