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.8.2
// @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, and overlay visibility
    const state = {
        isDragging: false,
        linkToPreload: null,
        popupWindow: null,
        acrylicOverlay: null,
        overlayVisible: false,
        preloadElement: null, // Track the created preload element
    };

    // 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),
        transitionDuration: 220, // 300ms 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 {
            // Remove any existing preload element to avoid duplication
            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();
        });
    }

    // 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.windowWidth}x${config.windowHeight})`, action: setWindowSize },
    ];

    // 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-10):', 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);
    }

    // Handle scroll event based on user settings
    function handleScrollCommand() {
        if (config.closeOnScroll) {
            window.addEventListener('scroll', closePopupOnScroll, { once: true });
        } else {
            window.removeEventListener('scroll', closePopupOnScroll);
        }
    }

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

    // Initialize menu commands
    updateMenuCommands();

    // Event listeners for drag-and-drop and other interactions
    document.body.addEventListener('dragstart', async (event) => {
        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('mousedown', async (event) => {
        const linkElement = event.target.tagName === 'A' ? event.target : event.target.closest('a');
        if (event.button === 0 && linkElement) { // Left mouse button
            const siteConfig = getSiteConfig();
            if (siteConfig && siteConfig.enableLeftClickPopup && !event.shiftKey) {
                // Custom behavior for specific sites: Prefetch and open popup
                await preloadLink(linkElement.href);
                openPopupWindow(linkElement.href, siteConfig.windowWidth, siteConfig.windowHeight);
                event.preventDefault();
            }
        }
    });

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

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

})();