知识星球视频缩放

在知识星球为视频添加一个美观的、不遮挡内容的布局缩放控制器。支持视频从中心突破边界放大,并自动推开上下文内容。

// ==UserScript==
// @name         知识星球视频缩放
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在知识星球为视频添加一个美观的、不遮挡内容的布局缩放控制器。支持视频从中心突破边界放大,并自动推开上下文内容。
// @author       Gemini、Claude、Lint
// @match        https://*.zsxq.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zsxq.com
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 全局变量 ---
    let currentTarget = null; // 当前正在控制的视频容器元素
    let currentGallery = null; // 当前的 gallery 元素

    // --- 样式定义 ---
    GM_addStyle(`
        /* 控制器容器的样式:固定在右下角,垂直紧凑设计 */
        .video-zoom-control-container-fixed {
            position: fixed;
            bottom: 20px;
            right: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 8px;
            padding: 12px;
            background-color: rgba(30, 30, 30, 0.85);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 16px;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.25);
            z-index: 9999;
            transition: opacity 0.3s ease, transform 0.3s ease;
            transform: translateY(20px);
            opacity: 0;
            pointer-events: none;
        }

        /* 控制器显示时的样式 */
        .video-zoom-control-container-fixed.visible {
            transform: translateY(0);
            opacity: 1;
            pointer-events: auto;
        }

        /* 标题样式 */
        .controller-title {
            font-size: 13px;
            font-weight: 600;
            color: #ffffff;
            padding-bottom: 4px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
            width: 100%;
            text-align: center;
        }

        /* 控件行的样式 */
        .control-row {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        /* 控制器图标样式 */
        .control-icon {
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            padding: 4px;
            border-radius: 6px;
            transition: background-color 0.2s ease;
        }
        .control-icon:hover {
            background-color: rgba(255, 255, 255, 0.1);
        }
        .control-icon svg {
            width: 18px;
            height: 18px;
            fill: #ffffff;
            transition: transform 0.2s ease;
        }
        .control-icon:hover svg {
            transform: scale(1.15);
        }

        /* 滑块样式 */
        .video-zoom-control-container-fixed input[type="range"] {
            width: 110px;
            cursor: pointer;
            margin: 0;
        }

        /* 百分比显示样式 */
        .video-zoom-control-container-fixed .zoom-percentage {
            font-size: 13px;
            font-weight: 600;
            color: #ffffff;
            min-width: 40px;
            text-align: center;
            font-family: monospace;
        }

        /* 视频缩放相关样式 (核心修改) */
        .zsxq-video-resizer-target {
            /* 修改1: 缩放原点改为中心 */
            transform-origin: center !important;
            /* 修改2: 为 transform 和 margin 添加平滑过渡效果 */
            transition: transform 0.2s ease-in-out, margin 0.2s ease-in-out !important;
        }
    `);

    /**
     * 【核心修改】应用缩放到视频容器
     * 通过 scale transform 放大视觉效果,同时用 margin 推开周围内容
     * @param {HTMLElement} target - 目标元素(视频的直接父容器)
     * @param {number} scale - 缩放比例(0.5-3.0)
     */
    function applyScale(target, scale) {
        if (!target) return;

        // 首次操作时,记录下元素的原始高度
        if (!target.dataset.originalHeight) {
            target.dataset.originalHeight = target.getBoundingClientRect().height;
        }
        const originalHeight = parseFloat(target.dataset.originalHeight);

        // 计算因缩放产生的额外高度,并将其均分为上下外边距
        let verticalMargin = 0;
        if (scale > 1) {
            // (缩放后的总高度 - 原始高度) / 2
            verticalMargin = (originalHeight * (scale - 1)) / 2;
        }

        // 应用 transform 和 margin
        target.style.transform = `scale(${scale})`;
        target.style.marginTop = `${verticalMargin}px`;
        target.style.marginBottom = `${verticalMargin}px`;

        console.log(`ZSXQ Resizer: 应用缩放 ${Math.round(scale * 100)}%`);
    }

    /**
     * 【核心修改】重置视频尺寸
     * @param {HTMLElement} target - 目标元素
     */
    function resetScale(target) {
        if (!target) return;

        // 恢复 transform 和 margin
        target.style.transform = 'scale(1)';
        target.style.marginTop = '0px';
        target.style.marginBottom = '0px';

        console.log('ZSXQ Resizer: 重置视频尺寸');
    }

    /**
     * 创建并初始化全局的缩放控制器
     * @returns {object} 包含控制器各个部分的元素对象
     */
    function createGlobalControls() {
        const container = document.createElement('div');
        container.className = 'video-zoom-control-container-fixed';

        const title = document.createElement('div');
        title.className = 'controller-title';
        title.textContent = '视频尺寸缩放';

        const controlRow = document.createElement('div');
        controlRow.className = 'control-row';

        const slider = document.createElement('input');
        slider.type = 'range';
        slider.min = 50;
        slider.max = 300;
        slider.value = 100;
        slider.step = 5;

        const percentageDisplay = document.createElement('span');
        percentageDisplay.className = 'zoom-percentage';
        percentageDisplay.textContent = '100%';

        const resetIcon = document.createElement('div');
        resetIcon.className = 'control-icon';
        resetIcon.title = '恢复原始尺寸';
        resetIcon.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 6v3l4-4l-4-4v3c-4.42 0-8 3.58-8 8c0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8c0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8c0 3.31-2.69 6-6 6v-3l-4 4l4 4v-3c4.42 0 8-3.58 8-8c0-1.57-.46-3.03-1.24-4.26z"/></svg>`;

        controlRow.append(slider, percentageDisplay, resetIcon);
        container.append(title, controlRow);
        document.body.appendChild(container);

        // --- 事件监听 ---
        const updateScale = (value) => {
            if (currentTarget) {
                const scaleValue = parseInt(value);
                const scaleFactor = scaleValue / 100;
                percentageDisplay.textContent = `${scaleValue}%`;
                applyScale(currentTarget, scaleFactor);
            }
        };

        slider.addEventListener('input', (e) => {
            updateScale(e.target.value);
        });

        resetIcon.addEventListener('click', () => {
            if (currentTarget) {
                slider.value = 100;
                percentageDisplay.textContent = '100%';
                resetScale(currentTarget);
            }
        });

        return { container, slider, percentageDisplay };
    }

    const controls = createGlobalControls();

    /**
     * 为指定的视频容器添加缩放控制功能
     * @param {HTMLElement} videoGallery - 包含 <video> 标签的 app-video-gallery 元素
     */
    function addZoomControls(videoGallery) {
        if (videoGallery.dataset.zoomControllerAdded) {
            return;
        }
        videoGallery.dataset.zoomControllerAdded = 'true';

        const videoElement = videoGallery.querySelector('video');
        if (!videoElement) {
            console.log('ZSXQ Resizer: 在 gallery 中未找到 video 元素');
            return;
        }

        const targetToResize = videoElement.parentElement;
        if (!targetToResize) {
            console.log('ZSXQ Resizer: 未找到视频的父容器');
            return;
        }

        // 【新增逻辑】在切换到新视频前,重置上一个视频的缩放状态
        if (currentTarget && currentTarget !== targetToResize) {
             resetScale(currentTarget);
        }

        targetToResize.classList.add('zsxq-video-resizer-target');

        // 设置为当前目标
        currentTarget = targetToResize;
        currentGallery = videoGallery;

        // 重置控制器状态并应用到新目标
        controls.slider.value = 100;
        controls.percentageDisplay.textContent = '100%';
        resetScale(currentTarget); // 确保新目标是默认状态

        // 显示控制器
        controls.container.classList.add('visible');

        console.log('ZSXQ Resizer: 已为视频添加缩放控制功能');
    }

    /**
     * 检查并处理页面上所有尚未处理的视频 gallery
     */
    function processVideoGalleries() {
        const galleries = document.querySelectorAll('app-video-gallery:not([data-zoom-controller-added])');

        if (galleries.length > 0) {
            if (currentTarget) {
                 controls.container.classList.remove('visible');
            }

            // 处理最新的视频
            const latestGallery = galleries[galleries.length - 1];
            addZoomControls(latestGallery);
        }
    }

    /**
     * 隐藏控制器并清除目标
     */
    function hideControls() {
        if (currentTarget) {
            resetScale(currentTarget);
        }
        currentTarget = null;
        currentGallery = null;
        controls.container.classList.remove('visible');
    }

    // --- 核心逻辑:使用 MutationObserver ---
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                let hasNewVideo = false;
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        const gallery = node.matches && node.matches('app-video-gallery') ? node :
                                        node.querySelector && node.querySelector('app-video-gallery');
                        if (gallery) {
                            hasNewVideo = true;
                        }
                    }
                });

                if (hasNewVideo) {
                    setTimeout(processVideoGalleries, 100);
                }
            }
        }
    });

    // 定期检查当前目标是否还在页面上
    setInterval(() => {
        if (currentGallery && !document.body.contains(currentGallery)) {
            hideControls();
        }
    }, 1000);

    // 首次加载时,先立即运行一次
    setTimeout(processVideoGalleries, 200);

    // 开始监听整个文档的变化
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();