Universal Video Image Control

On-screen slider control panel for Gamma, Brightness, Contrast, and Saturation with cross-origin iframe sync, icon toggle, active switch, isolated Shadow DOM, custom track gradients, and dimmed inactive states.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         Universal Video Image Control
// @namespace    https://github.com/
// @version      3.0
// @description  On-screen slider control panel for Gamma, Brightness, Contrast, and Saturation with cross-origin iframe sync, icon toggle, active switch, isolated Shadow DOM, custom track gradients, and dimmed inactive states.
// @match        *://*/*
// @allFrames    true
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const S_GAMMA = 'vid_ctrl_gamma';
    const S_BRIGHT = 'vid_ctrl_bright';
    const S_CONTRAST = 'vid_ctrl_contrast';
    const S_SATURATE = 'vid_ctrl_saturate';
    const S_ACTIVE = 'vid_ctrl_active';

    let vals = {
        gamma: parseFloat(localStorage.getItem(S_GAMMA)) || 1.0,
        brightness: parseFloat(localStorage.getItem(S_BRIGHT)) || 1.0,
        contrast: parseFloat(localStorage.getItem(S_CONTRAST)) || 1.0,
        saturation: parseFloat(localStorage.getItem(S_SATURATE)) || 1.0,
        active: localStorage.getItem(S_ACTIVE) !== 'false'
    };

    let container = null;
    let icon = null;
    let panel = null;
    let hideTimeout = null;
    let isHovering = false;
    let isPanelOpen = false;

    // --- Cross-Origin Sync Logic ---
    function broadcastSync() {
        localStorage.setItem(S_GAMMA, vals.gamma);
        localStorage.setItem(S_BRIGHT, vals.brightness);
        localStorage.setItem(S_CONTRAST, vals.contrast);
        localStorage.setItem(S_SATURATE, vals.saturation);
        localStorage.setItem(S_ACTIVE, vals.active);

        const payload = { app: 'VidCtrl', action: 'sync_down', vals: vals };

        if (window !== window.top) {
            window.top.postMessage({ app: 'VidCtrl', action: 'sync_up', vals: vals }, '*');
        } else {
            document.querySelectorAll('iframe').forEach(ifr => {
                try { ifr.contentWindow.postMessage(payload, '*'); } catch(e){}
            });
        }
    }

    window.addEventListener('message', (e) => {
        if (!e.data || e.data.app !== 'VidCtrl') return;

        if (e.data.action === 'request_sync' && window === window.top) {
            if (e.source) e.source.postMessage({ app: 'VidCtrl', action: 'sync_down', vals: vals }, '*');
        }
        else if (e.data.action === 'sync_up' && window === window.top) {
            vals = Object.assign({}, e.data.vals);
            localStorage.setItem(S_GAMMA, vals.gamma);
            localStorage.setItem(S_BRIGHT, vals.brightness);
            localStorage.setItem(S_CONTRAST, vals.contrast);
            localStorage.setItem(S_SATURATE, vals.saturation);
            localStorage.setItem(S_ACTIVE, vals.active);

            applyFilters();
            updateUI();

            document.querySelectorAll('iframe').forEach(ifr => {
                if (ifr.contentWindow !== e.source) {
                    try { ifr.contentWindow.postMessage({ app: 'VidCtrl', action: 'sync_down', vals: vals }, '*'); } catch(err){}
                }
            });
        }
        else if (e.data.action === 'sync_down') {
            vals = Object.assign({}, e.data.vals);
            applyFilters();
            updateUI();

            document.querySelectorAll('iframe').forEach(ifr => {
                try { ifr.contentWindow.postMessage(e.data, '*'); } catch(err){}
            });
        }
    });

    if (window !== window.top) {
        window.top.postMessage({ app: 'VidCtrl', action: 'request_sync' }, '*');
    }
    // -------------------------------

    function updateSliderTrack(el) {
        if (!el) return;
        const min = parseFloat(el.min) || 0;
        const max = parseFloat(el.max) || 100;
        const val = parseFloat(el.value) || 0;
        const pct = ((val - min) / (max - min)) * 100;

        // Multi-layered background: semi-transparent red layer on top of the base dark gray gradient track (dark to light)
        el.style.setProperty('background', `linear-gradient(to right, rgba(255, 51, 51, 0.5) 0%, rgba(255, 51, 51, 0.5) ${pct}%, transparent ${pct}%, transparent 100%), linear-gradient(to right, #1a1a1a 0%, #2f2f2f 33.5%, #444444 100%)`, 'important');    }

    function createUI() {
        if (document.getElementById('video-img-ctrl-container')) {
            container = document.getElementById('video-img-ctrl-container');
            return;
        }

        container = document.createElement('div');
        container.id = 'video-img-ctrl-container';

        container.style.cssText = `
            position: fixed !important;
            top: 10px !important;
            right: 10px !important;
            z-index: 2147483647 !important;
            opacity: 0 !important;
            pointer-events: none !important;
            transition: opacity 0.3s ease-in-out !important;
        `;

        const shadow = container.attachShadow({ mode: 'open' });

        const style = document.createElement('style');
        style.textContent = `
            :host {
                display: flex !important;
                flex-direction: column !important;
                align-items: flex-end !important;
                gap: 8px !important;
            }
            .vid-ctrl-icon {
                background: rgba(0, 0, 0, 0.80) !important;
                backdrop-filter: blur(4px) !important;
                color: #fff !important;
                width: 32px !important;
                height: 32px !important;
                border-radius: 6px !important;
                display: flex !important;
                justify-content: center !important;
                align-items: center !important;
                cursor: pointer !important;
                box-shadow: 0 4px 15px rgba(0,0,0,0.8) !important;
            }
            .vid-ctrl-panel {
                background: rgba(0, 0, 0, 0.80) !important;
                backdrop-filter: blur(4px) !important;
                color: #fff !important;
                padding: 10px !important;
                border-radius: 6px !important;
                font-family: system-ui, -apple-system, sans-serif !important;
                font-size: 14px !important;
                box-shadow: 0 4px 15px rgba(0,0,0,0.8) !important;
                border: none !important;
                display: none !important;
                flex-direction: column !important;
                gap: 8px !important;
                user-select: none !important;
                width: 440px !important;
                box-sizing: border-box !important;
            }
            .row {
                display: flex !important;
                justify-content: space-between !important;
                align-items: center !important;
                width: 100% !important;
                box-sizing: border-box !important;
                transition: opacity 0.2s ease !important;
            }
            label {
                margin: 0 !important;
                font-weight: 500 !important;
                color: #fff !important;
                text-align: left !important;
                font-size: 14px !important;
                flex-shrink: 0 !important;
            }
            .label-slider { width: 85px !important; }
            .span-val {
                min-width: 40px !important;
                text-align: right !important;
                font-weight: 500 !important;
                color: #fff !important;
                font-size: 14px !important;
                flex-shrink: 0 !important;
            }
            .vid-ctrl-slider {
                -webkit-appearance: none !important;
                appearance: none !important;
                flex-grow: 1 !important;
                margin: 0 8px !important;
                cursor: pointer !important;
                min-width: 0 !important;
                box-sizing: border-box !important;
                height: 6px !important;
                border-radius: 3px !important;
                outline: none !important;
            }
            .vid-ctrl-slider::-webkit-slider-thumb {
                -webkit-appearance: none !important;
                appearance: none !important;
                width: 14px !important;
                height: 14px !important;
                border-radius: 50% !important;
                background: #ff3333 !important;
                cursor: pointer !important;
                border: none !important;
            }
            .vid-ctrl-slider::-moz-range-thumb {
                width: 14px !important;
                height: 14px !important;
                border-radius: 50% !important;
                background: #ff3333 !important;
                cursor: pointer !important;
                border: none !important;
            }
            .footer {
                display: flex !important;
                justify-content: space-between !important;
                align-items: center !important;
                width: 100% !important;
                margin-top: 4px !important;
                box-sizing: border-box !important;
            }
            .footer-label {
                display: flex !important;
                align-items: center !important;
                gap: 6px !important;
                cursor: pointer !important;
                user-select: none !important;
                font-size: 14px !important;
            }
            .vid-ctrl-checkbox {
                accent-color: #ff3333 !important;
                margin: 0 !important;
                cursor: pointer !important;
                width: 15px !important;
                height: 15px !important;
            }
            #ctrl-reset {
                background: #333 !important;
                color: #fff !important;
                border: 1px solid #555 !important;
                padding: 4px 16px !important;
                border-radius: 4px !important;
                cursor: pointer !important;
                font-size: 13px !important;
                font-weight: 500 !important;
                box-sizing: border-box !important;
                transition: opacity 0.2s ease !important;
            }
            /* Visual gray-out state rules when filters are inactive */
            .vid-ctrl-panel.is-inactive .row,
            .vid-ctrl-panel.is-inactive #ctrl-reset {
                opacity: 0.35 !important;
                pointer-events: none !important;
            }
        `;
        shadow.appendChild(style);

        icon = document.createElement('div');
        icon.className = 'vid-ctrl-icon';
        icon.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>`;

        panel = document.createElement('div');
        panel.className = 'vid-ctrl-panel';
        if (!vals.active) panel.classList.add('is-inactive');

        const createRow = (id, label, min, max, val) => `
            <div class="row">
                <label for="${id}" class="label-slider">${label}</label>
                <input type="range" id="${id}" class="vid-ctrl-slider" min="${min}" max="${max}" step="0.05" value="${val}">
                <span id="${id}-val" class="span-val">${val.toFixed(2)}</span>
            </div>
        `;

        panel.innerHTML = `
            ${createRow('ctrl-gamma', 'Gamma', 0.5, 2.0, vals.gamma)}
            ${createRow('ctrl-bright', 'Brightness', 0.5, 2.0, vals.brightness)}
            ${createRow('ctrl-contrast', 'Contrast', 0.5, 2.0, vals.contrast)}
            ${createRow('ctrl-saturate', 'Saturation', 0.0, 3.0, vals.saturation)}
            <div class="footer">
                <label class="footer-label">
                    <input type="checkbox" id="ctrl-active" class="vid-ctrl-checkbox" ${vals.active ? 'checked' : ''}>
                    Active
                </label>
                <button id="ctrl-reset">Reset All</button>
            </div>
        `;

        shadow.appendChild(icon);
        shadow.appendChild(panel);

        panel.querySelectorAll('.vid-ctrl-slider').forEach(updateSliderTrack);

        attachUI();
        bindEvents();
    }

    function bindEvents() {
        icon.addEventListener('click', () => {
            isPanelOpen = !isPanelOpen;
            panel.style.setProperty('display', isPanelOpen ? 'flex' : 'none', 'important');
        });

        panel.querySelector('#ctrl-active').addEventListener('change', (e) => {
            vals.active = e.target.checked;
            if (vals.active) {
                panel.classList.remove('is-inactive');
            } else {
                panel.classList.add('is-inactive');
            }
            applyFilters();
            broadcastSync();
        });

        const inputs = [
            { id: 'ctrl-gamma', key: 'gamma' },
            { id: 'ctrl-bright', key: 'brightness' },
            { id: 'ctrl-contrast', key: 'contrast' },
            { id: 'ctrl-saturate', key: 'saturation' }
        ];

        inputs.forEach(item => {
            const el = panel.querySelector(`#${item.id}`);
            const display = panel.querySelector(`#${item.id}-val`);

            el.addEventListener('input', (e) => {
                vals[item.key] = parseFloat(e.target.value);
                display.textContent = vals[item.key].toFixed(2);
                updateSliderTrack(el);
                applyFilters();
                broadcastSync();
            });
        });

        panel.querySelector('#ctrl-reset').addEventListener('click', () => {
            vals = { gamma: 1.0, brightness: 1.0, contrast: 1.0, saturation: 1.0, active: true };
            updateUI();
            applyFilters();
            broadcastSync();
        });

        container.addEventListener('mouseenter', () => {
            isHovering = true;
            showUI();
        });

        container.addEventListener('mouseleave', () => {
            isHovering = false;
            queueHide();
        });
    }

    function updateUI() {
        if (!panel) return;

        if (vals.active) {
            panel.classList.remove('is-inactive');
        } else {
            panel.classList.add('is-inactive');
        }

        const inputs = [
            { id: 'ctrl-gamma', key: 'gamma' },
            { id: 'ctrl-bright', key: 'brightness' },
            { id: 'ctrl-contrast', key: 'contrast' },
            { id: 'ctrl-saturate', key: 'saturation' }
        ];
        inputs.forEach(item => {
            const el = panel.querySelector(`#${item.id}`);
            const display = panel.querySelector(`#${item.id}-val`);
            if (el && display) {
                el.value = vals[item.key];
                display.textContent = vals[item.key].toFixed(2);
                updateSliderTrack(el);
            }
        });
        const activeCb = panel.querySelector('#ctrl-active');
        if (activeCb) {
            activeCb.checked = vals.active;
        }
    }

    function attachUI() {
        if (!container) return;
        const target = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement || document.body;
        if (container.parentNode !== target) {
            target.appendChild(container);
        }
    }

    function applyFilters() {
        const videos = document.querySelectorAll('video');

        if (!vals.active) {
            videos.forEach(video => {
                video.style.setProperty('filter', 'none', 'important');
            });
            return;
        }

        const b = vals.gamma * vals.brightness;
        const c = (1 / vals.gamma) * vals.contrast;
        const s = vals.saturation;

        videos.forEach(video => {
            video.style.setProperty('filter', `brightness(${b}) contrast(${c}) saturate(${s})`, 'important');
        });
    }

    function showUI() {
        if (!container) return;
        container.style.setProperty('opacity', '1.0', 'important');
        container.style.setProperty('pointer-events', 'auto', 'important');
        clearTimeout(hideTimeout);
        if (!isHovering) {
            queueHide();
        }
    }

    function queueHide() {
        clearTimeout(hideTimeout);
        hideTimeout = setTimeout(() => {
            if (container && !isHovering) {
                container.style.setProperty('opacity', '0', 'important');
                container.style.setProperty('pointer-events', 'none', 'important');

                if (isPanelOpen) {
                    isPanelOpen = false;
                    panel.style.setProperty('display', 'none', 'important');
                }
            }
        }, 3000);
    }

    document.addEventListener('mousemove', showUI);
    document.addEventListener('fullscreenchange', attachUI);
    document.addEventListener('webkitfullscreenchange', attachUI);

    const observer = new MutationObserver(() => {
        if (document.querySelector('video')) {
            createUI();
            applyFilters();
            attachUI();
        }
    });

    observer.observe(document.body || document.documentElement, { childList: true, subtree: true });

    if (document.querySelector('video')) {
        createUI();
        applyFilters();
    }
})();