Greasy Fork 支持简体中文。

Ultimate Picture-in-Picture Enhancer

Automatically enable PIP mode with a smooth transition and a configurable, centered control panel.

// ==UserScript==
// @name         Ultimate Picture-in-Picture Enhancer
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Automatically enable PIP mode with a smooth transition and a configurable, centered control panel.
// @author       OB_BUFF
// @license      GPL-3.0
// @match        *://*/*
// @grant        GM_notification
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';

    // Load saved settings or use defaults
    let pipAnimationEnabled = GM_getValue("pipAnimationEnabled", true);
    let notificationEnabled = GM_getValue("notificationEnabled", true);
    let pipThreshold = GM_getValue("pipThreshold", 0.3);
    let pipActive = false;
    const iconUrl = "https://images.sftcdn.net/images/t_app-icon-m/p/e858578e-7424-4b99-a13f-c57cd65f8017/4229007087/pip-it-picture-in-picture-logo";

    // Multi-language support for UI texts
    const messages = {
        "en": {
            "enterPiP": "Page lost focus, video entered Picture-in-Picture mode",
            "exitPiP": "Page is back in focus, exiting Picture-in-Picture mode",
            "pipSettings": "PIP Enhancer Settings",
            "enableAnimation": "Enable Animation",
            "disableAnimation": "Disable Animation",
            "enableNotifications": "Enable Notifications",
            "disableNotifications": "Disable Notifications",
            "pipThreshold": "PIP Trigger Ratio"
        },
        "zh": {
            "enterPiP": "网页失去焦点,视频进入画中画模式",
            "exitPiP": "网页回到前台,退出画中画模式",
            "pipSettings": "画中画增强设置",
            "enableAnimation": "启用动画",
            "disableAnimation": "禁用动画",
            "enableNotifications": "启用通知",
            "disableNotifications": "禁用通知",
            "pipThreshold": "PIP 触发比例"
        },
        "es": {
            "enterPiP": "La página perdió el foco, el video entró en modo PiP",
            "exitPiP": "La página volvió a enfocarse, saliendo del modo PiP",
            "pipSettings": "Configuración de PIP Enhancer",
            "enableAnimation": "Habilitar animación",
            "disableAnimation": "Deshabilitar animación",
            "enableNotifications": "Habilitar notificaciones",
            "disableNotifications": "Deshabilitar notificaciones",
            "pipThreshold": "Proporción de activación de PiP"
        }
    };

    // Detect browser language (default to English)
    const userLang = navigator.language.startsWith("zh") ? "zh" :
        navigator.language.startsWith("es") ? "es" : "en";

    // Save current settings
    function saveSettings() {
        GM_setValue("pipAnimationEnabled", pipAnimationEnabled);
        GM_setValue("notificationEnabled", notificationEnabled);
        GM_setValue("pipThreshold", pipThreshold);
    }

    // Add a single menu command to open the control panel
    GM_registerMenuCommand(messages[userLang].pipSettings, openControlPanel);

    /**
     * Checks if a video meets the PIP criteria:
     * - Playing
     * - Has sound (volume > 0 and not muted)
     * - Covers at least pipThreshold of the screen area
     */
    function isEligibleVideo(video) {
        const rect = video.getBoundingClientRect();
        const screenArea = window.innerWidth * window.innerHeight;
        const videoArea = rect.width * rect.height;
        return (
            !video.paused &&
            video.volume > 0 && !video.muted &&
            (videoArea / screenArea) > pipThreshold
        );
    }

    /**
     * Enters Picture-in-Picture mode.
     */
    async function enterPiP() {
        if (pipActive) return;
        const videos = document.querySelectorAll("video");
        for (let video of videos) {
            if (isEligibleVideo(video)) {
                try {
                    if (pipAnimationEnabled) animatePiP(video);
                    await video.requestPictureInPicture();
                    pipActive = true;
                    if (notificationEnabled) {
                        GM_notification({
                            text: messages[userLang].enterPiP,
                            title: messages[userLang].pipSettings,
                            timeout: 5000,
                            image: iconUrl
                        });
                    }
                } catch (error) {
                    console.error("Unable to enter PIP mode:", error);
                }
                break;
            }
        }
    }

    /**
     * Exits Picture-in-Picture mode.
     */
    function exitPiP() {
        if (!pipActive) return;
        if (document.pictureInPictureElement) {
            document.exitPictureInPicture();
            if (notificationEnabled) {
                GM_notification({
                    text: messages[userLang].exitPiP,
                    title: messages[userLang].pipSettings,
                    timeout: 5000,
                    image: iconUrl
                });
            }
        }
        pipActive = false;
    }

    /**
     * Applies a smooth animation effect to the video element before PIP activation.
     */
    function animatePiP(video) {
        video.style.transition = "transform 0.7s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.7s cubic-bezier(0.4, 0, 0.2, 1)";
        video.style.transform = "scale(0.9)";
        video.style.opacity = "0.8";
        setTimeout(() => {
            video.style.transform = "scale(1)";
            video.style.opacity = "1";
        }, 700);
    }

    /**
     * Opens a centered HTML control panel that allows users to configure settings.
     */
    function openControlPanel() {
        // Create panel container
        let panel = document.createElement("div");
        panel.id = "pip-control-panel";
        panel.innerHTML = `
            <div class="pip-panel-inner">
                <h2>${messages[userLang].pipSettings}</h2>
                <div>
                    <label>
                        <input type="checkbox" id="pipAnimationCheckbox">
                        ${messages[userLang].enableAnimation}
                    </label>
                </div>
                <div>
                    <label>
                        <input type="checkbox" id="pipNotificationsCheckbox">
                        ${messages[userLang].enableNotifications}
                    </label>
                </div>
                <div>
                    <label>
                        ${messages[userLang].pipThreshold}:
                        <input type="number" id="pipThresholdInput" value="${pipThreshold}" step="0.1" min="0" max="1">
                    </label>
                </div>
                <button id="pipSaveSettings">Save</button>
                <button id="pipClosePanel">Close</button>
            </div>
        `;
        document.body.appendChild(panel);
    }

    // Add some CSS for the control panel using GM_addStyle
    GM_addStyle(`
        #pip-control-panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #222;
            color: #fff;
            padding: 20px;
            border-radius: 8px;
            z-index: 10000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            width: 300px;
            font-family: sans-serif;
        }
        #pip-control-panel .pip-panel-inner {
            text-align: center;
        }
        #pip-control-panel h2 {
            margin-top: 0;
            font-size: 20px;
        }
        #pip-control-panel label {
            display: block;
            margin: 10px 0;
            font-size: 14px;
        }
        #pip-control-panel input[type="number"] {
            width: 60px;
            margin-left: 5px;
        }
        #pip-control-panel button {
            margin: 10px 5px 0;
            padding: 5px 10px;
            background: #555;
            border: none;
            border-radius: 4px;
            color: #fff;
            cursor: pointer;
        }
        #pip-control-panel button:hover {
            background: #666;
        }
    `);

    // Event delegation for the control panel buttons (using event listeners on document)
    document.addEventListener("click", function (e) {
        if (e.target && e.target.id === "pipSaveSettings") {
            // Save the settings from the control panel
            pipAnimationEnabled = document.getElementById("pipAnimationCheckbox").checked;
            notificationEnabled = document.getElementById("pipNotificationsCheckbox").checked;
            pipThreshold = parseFloat(document.getElementById("pipThresholdInput").value);
            saveSettings();
            document.getElementById("pip-control-panel").remove();
        }
        if (e.target && e.target.id === "pipClosePanel") {
            document.getElementById("pip-control-panel").remove();
        }
    });

    // When the control panel is opened, pre-check the current settings.
    document.addEventListener("click", function (e) {
        if (e.target && e.target.id === "pip-control-panel") {
            // do nothing here
        }
    });

    // Pre-populate control panel checkboxes when panel is added.
    const observer = new MutationObserver((mutationsList, observer) => {
        const panel = document.getElementById("pip-control-panel");
        if (panel) {
            document.getElementById("pipAnimationCheckbox").checked = pipAnimationEnabled;
            document.getElementById("pipNotificationsCheckbox").checked = notificationEnabled;
        }
    });
    observer.observe(document.body, { childList: true });

    /**
     * Listen for visibility changes to trigger PIP.
     */
    document.addEventListener("visibilitychange", function () {
        if (document.hidden) {
            setTimeout(() => {
                if (document.hidden) enterPiP();
            }, 300);
        } else {
            exitPiP();
        }
    });

    /**
     * Listen for window focus changes.
     */
    window.addEventListener("blur", enterPiP);
    window.addEventListener("focus", exitPiP);

})();