Ultimate Picture-in-Picture Enhancer

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();