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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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