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.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Advertisement:

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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