MDK Orange TV

Ajuste la grille de Orange TV, gère le volume au clavier/souris, cache les chaînes non souscrites et offre des raccourcis.

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!)

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!)

// ==UserScript==
// @name         MDK Orange TV
// @namespace    MDK Scripts
// @version      2026.05.26.7
// @description  Ajuste la grille de Orange TV, gère le volume au clavier/souris, cache les chaînes non souscrites et offre des raccourcis.
// @author       MDK
// @license      MIT
// @match        https://tv.orange.fr/*
// @run-at       document-start
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tv.orange.fr
// ==/UserScript==

(function() {
    'use strict';

    // Configuration des constantes globales et des limites de valeurs
    const ICON_ORANGETV = "https://www.google.com/s2/favicons?sz=64&domain=tv.orange.fr";
    const PLAY_URL_REGEX = /^https?:\/\/.*tv\.orange\.fr\/lecture\/en-direct\/chaines\/livetv.*/;

    const DEFAULTS = { nbreCol: 4, gapX: 18, osdSize: 48, hideChannels: true };
    const LIMITS = {
        nbreCol: { min: 2, max: 8 },
        gapX: { min: 6, max: 64 },
        osdSize: { min: 16, max: 72 }
    };

    const clamp = (val, min, max) => Math.max(min, Math.min(max, val));

    // Chargement de la configuration utilisateur avec repli sur les valeurs par défaut
    const config = {
        nbreCol: clamp(parseInt(GM_getValue('nbreCol', DEFAULTS.nbreCol), 10), LIMITS.nbreCol.min, LIMITS.nbreCol.max),
        gapX: clamp(parseInt(GM_getValue('gapX', DEFAULTS.gapX), 10), LIMITS.gapX.min, LIMITS.gapX.max),
        osdSize: clamp(parseInt(GM_getValue('osdSize', DEFAULTS.osdSize), 10), LIMITS.osdSize.min, LIMITS.osdSize.max),
        hideChannels: GM_getValue('hideChannels', DEFAULTS.hideChannels)
    };

    const syncChannel = new BroadcastChannel('mdk_orange_tv_sync');

    // Injection des styles CSS de base pour l'interface et le masquage des chaînes
    GM_addStyle(`
        :root {
            --popup-bg: #ffffff;
            --popup-text: #333333;
            --popup-border: #cccccc;
            --popup-input-bg: #ffffff;
            --halo-color: rgba(241, 110, 0, 0.5);
            --focus-outline: #f16e00;
            --mdk-orange-cols: ${config.nbreCol};
            --mdk-orange-gap: ${config.gapX}px;
            --osd-font-size: ${config.osdSize}px;
        }
        @media (prefers-color-scheme: dark) {
            :root {
                --popup-bg: #1f1f1f;
                --popup-text: #f0f0f0;
                --popup-border: #444444;
                --popup-input-bg: #2d2d2d;
                --halo-color: rgba(241, 110, 0, 0.7);
            }
        }

        :root.mdk-hide-channels #listing-mosaic li:has(div.corner),
        :root.mdk-hide-channels .section-container .stvui-strip:has(img[src*="buy-white.png"]),
        :root.mdk-hide-channels .stvui-vertical-list li:has(div.corner),
        :root.mdk-hide-channels .stvui-mosaic li:has(div.corner) {
            display: none !important;
        }

        .stvui-mosaic.medium.landscape {
            --nb-media: var(--mdk-orange-cols) !important;
            --media-horizontal-gap: var(--mdk-orange-gap) !important;
        }

        .gm-overlay {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background-color: rgba(0,0,0,0.6); display: flex;
            justify-content: center; align-items: center; z-index: 2147483647; font-family: sans-serif;
            animation: fadeIn 0.2s ease-out;
        }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

        .gm-popup {
            background-color: var(--popup-bg); color: var(--popup-text);
            padding: 22px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.4);
            width: 420px; display: flex; flex-direction: column; gap: 16px;
            border: 1px solid var(--popup-border); box-sizing: border-box;
        }
        .gm-popup-header { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
        .gm-popup-icon { width: 28px; height: 28px; object-fit: contain; }
        .gm-popup-title { margin: 0; font-size: 18px; font-weight: bold; }
        .gm-popup-row { display: flex; justify-content: space-between; align-items: center; width: 100%; }

        .gm-popup-help {
            font-size: 11px; color: #888888; border-top: 1px dashed var(--popup-border);
            padding-top: 10px; margin-top: 4px; line-height: 1.5;
        }

        .gm-popup-footer { display: flex; justify-content: space-between; margin-top: 4px; gap: 10px; width: 100%; }

        .gm-popup-input {
            width: 75px; padding: 6px 10px; background: var(--popup-input-bg);
            color: var(--popup-text); border: 1px solid var(--popup-border);
            border-radius: 4px; text-align: center; box-sizing: border-box;
            font-size: 14px;
        }
        .gm-popup-input:focus, .gm-btn-action:focus, #input-hideChannels:focus { outline: 2px solid var(--focus-outline) !important; }

        .gm-btn-action {
            flex: 1; padding: 10px 14px; color: #fff; border: none;
            border-radius: 4px; cursor: pointer; font-weight: bold;
            transition: background 0.15s ease; text-align: center;
            font-size: 14px; white-space: nowrap;
        }
        #btn-save { background: #f16e00; animation: gm-pulse-halo 2s infinite; }
        #btn-save:focus { animation: none; }
        #btn-cancel { background: #6c757d; }
        #btn-reset { background: #dc3545; }

        #btn-save:hover { background: #d66100; }
        #btn-cancel:hover { background: #5a6268; }
        #btn-reset:hover { background: #bd2130; }

        @keyframes gm-pulse-halo {
            0% { box-shadow: 0 0 0 0 var(--halo-color); }
            70% { box-shadow: 0 0 0 8px rgba(241, 110, 0, 0); }
            100% { box-shadow: 0 0 0 0 rgba(241, 110, 0, 0); }
        }

        .gm-volume-indicator {
            position: fixed; top: 30px; right: 30px; background: rgba(0, 0, 0, 0.85);
            color: #fff; padding: 12px 24px; border-radius: 16px; font-family: sans-serif;
            font-size: var(--osd-font-size); font-weight: bold; z-index: 2147483647;
            pointer-events: none; transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
            opacity: 0; transform: translateY(-20px);
            display: flex; flex-direction: column; align-items: center; gap: 6px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1);
        }
        .gm-volume-indicator.show { opacity: 1; transform: translateY(0); }
        .gm-vol-bar-container { width: 100%; height: 6px; background: rgba(255,255,255,0.2); border-radius: 3px; overflow: hidden; margin-top: 4px;}
        .gm-vol-bar { height: 100%; background: #f16e00; width: 0%; transition: width 0.15s ease-out, background 0.3s ease; }
    `);

    // Application dynamique des variables CSS sur l'élément racine du document
    function applyLiveStyle(cols, gap, osdSize, hideChannels) {
        const root = document.documentElement;
        if (!root) return;
        root.style.setProperty('--mdk-orange-cols', cols);
        root.style.setProperty('--mdk-orange-gap', `${gap}px`);
        root.style.setProperty('--osd-font-size', `${osdSize}px`);

        if (hideChannels) {
            root.classList.add('mdk-hide-channels');
        } else {
            root.classList.remove('mdk-hide-channels');
        }
    }

    // Synchronisation globale de l'affichage avec l'état de configuration actuel
    function updateCSSVariables() {
        applyLiveStyle(config.nbreCol, config.gapX, config.osdSize, config.hideChannels);
    }

    // Détermination du conteneur parent approprié pour l'affichage des éléments graphiques
    function getContainer() {
        return document.fullscreenElement || document.body || document.documentElement;
    }

    let volumeTimeout = null;

    // Gestion de l'affichage graphique de la barre d'OSD pour le volume
    function showVolumeOSD(video) {
        const container = getContainer();
        if (!container) return;

        let indicator = document.getElementById('gm-vol-osd');

        if (!indicator) {
            indicator = document.createElement('div');
            indicator.id = 'gm-vol-osd';
            indicator.className = 'gm-volume-indicator';
            indicator.innerHTML = `<span id="gm-vol-text"></span><div class="gm-vol-bar-container"><div id="gm-vol-bar" class="gm-vol-bar"></div></div>`;
            container.appendChild(indicator);
        } else if (indicator.parentNode !== container) {
            container.appendChild(indicator);
        }

        const textEl = document.getElementById('gm-vol-text');
        const barEl = document.getElementById('gm-vol-bar');

        if (!textEl || !barEl) return;

        const volPercent = video.muted ? 0 : Math.round(video.volume * 100);
        const icon = video.muted || volPercent === 0 ? '🔇' : volPercent < 40 ? '🔈' : volPercent < 80 ? '🔉' : '🔊';

        textEl.textContent = `${icon} ${volPercent}%`;
        barEl.style.width = `${volPercent}%`;
        barEl.style.background = video.muted ? '#6c757d' : '#f16e00';

        indicator.classList.add('show');

        clearTimeout(volumeTimeout);
        volumeTimeout = setTimeout(() => { indicator.classList.remove('show'); }, 2000);
    }

    // Ajustement du niveau de volume de l'élément vidéo actif
    function adjustVolume(step) {
        if (!PLAY_URL_REGEX.test(window.location.href)) return;
        const video = document.querySelector('video');
        if (!video) return;

        video.muted = false;
        let currentVol = Math.round(video.volume * 100);
        let newVol = clamp(currentVol + step, 0, 100);

        video.volume = newVol / 100;
        showVolumeOSD(video);
    }

    // Interception des actions de la molette de la souris pour contrôler le volume
    function handleVolumeWheel(e) {
        if (!PLAY_URL_REGEX.test(window.location.href)) return;
        if (e.target.closest('#gm-settings-popup, .stvui-vertical-list')) return;

        const style = window.getComputedStyle(e.target);
        if (e.target.scrollHeight > e.target.clientHeight && (style.overflowY === 'auto' || style.overflowY === 'scroll')) {
            return;
        }

        e.preventDefault();
        const step = e.deltaY < 0 ? 5 : -5;
        adjustVolume(step);
    }

    window.addEventListener('wheel', handleVolumeWheel, { passive: false });

    // Génération et orchestration des événements de la popup de configuration
    function createSettingsPopup() {
        if (document.getElementById('gm-settings-popup')) return;

        const container = getContainer();
        if (!container) return;

        const initialCols = config.nbreCol;
        const initialGap = config.gapX;
        const initialOsdSize = config.osdSize;
        const initialHideChannels = config.hideChannels;

        const overlay = document.createElement('div');
        overlay.id = 'gm-settings-popup';
        overlay.className = 'gm-overlay';

        const popup = document.createElement('div');
        popup.className = 'gm-popup';
        popup.setAttribute('role', 'dialog');
        popup.setAttribute('aria-modal', 'true');

        popup.innerHTML = `
            <div class="gm-popup-header">
                <img src="${ICON_ORANGETV}" class="gm-popup-icon" alt="Orange TV">
                <h3 class="gm-popup-title">Configuration Orange TV</h3>
            </div>
            <div class="gm-popup-row">
                <label for="input-nbreCol">Nombre de colonnes (${LIMITS.nbreCol.min}-${LIMITS.nbreCol.max}) :</label>
                <input type="number" id="input-nbreCol" class="gm-popup-input" value="${config.nbreCol}" min="${LIMITS.nbreCol.min}" max="${LIMITS.nbreCol.max}">
            </div>
            <div class="gm-popup-row">
                <label for="input-gapX">Espacement horizontal (${LIMITS.gapX.min}-${LIMITS.gapX.max}px) :</label>
                <input type="number" id="input-gapX" class="gm-popup-input" value="${config.gapX}" min="${LIMITS.gapX.min}" max="${LIMITS.gapX.max}">
            </div>
            <div class="gm-popup-row">
                <label for="input-osdSize">Taille de l'OSD (${LIMITS.osdSize.min}-${LIMITS.osdSize.max}px) :</label>
                <input type="number" id="input-osdSize" class="gm-popup-input" value="${config.osdSize}" min="${LIMITS.osdSize.min}" max="${LIMITS.osdSize.max}">
            </div>
            <div class="gm-popup-row">
                <label for="input-hideChannels">Cacher les chaînes non souscrites :</label>
                <input type="checkbox" id="input-hideChannels" ${config.hideChannels ? 'checked' : ''} style="cursor: pointer; width: 18px; height: 18px;">
            </div>
            <div class="gm-popup-help">
                <strong>Raccourcis clavier :</strong><br>
                • <kbd>[O]</kbd> : Ouvrir / Fermer cette interface de configuration<br>
                • <kbd>[H]</kbd> : Afficher / Masquer les chaînes non souscrites
            </div>
            <div class="gm-popup-footer">
                <button id="btn-save" class="gm-btn-action" type="button"><u>E</u>nregistrer</button>
                <button id="btn-cancel" class="gm-btn-action" type="button"><u>A</u>nnuler</button>
                <button id="btn-reset" class="gm-btn-action" type="button"><u>R</u>éinitialiser</button>
            </div>
        `;

        overlay.appendChild(popup);
        container.appendChild(overlay);

        const inputCol = document.getElementById('input-nbreCol');
        const inputGap = document.getElementById('input-gapX');
        const inputOsd = document.getElementById('input-osdSize');
        const checkboxHide = document.getElementById('input-hideChannels');

        const inputElements = [inputCol, inputGap, inputOsd];
        const focusableElements = Array.from(popup.querySelectorAll('input, button'));
        focusableElements[0].focus();

        // Traitement des changements de saisie et prévisualisation instantanée
        const triggerLiveUpdate = () => {
            const c = clamp(parseInt(inputCol.value, 10) || DEFAULTS.nbreCol, LIMITS.nbreCol.min, LIMITS.nbreCol.max);
            const g = clamp(parseInt(inputGap.value, 10) || DEFAULTS.gapX, LIMITS.gapX.min, LIMITS.gapX.max);
            const os = clamp(parseInt(inputOsd.value, 10) || DEFAULTS.osdSize, LIMITS.osdSize.min, LIMITS.osdSize.max);
            const hc = checkboxHide.checked;

            applyLiveStyle(c, g, os, hc);
            syncChannel.postMessage({ type: 'preview', nbreCol: c, gapX: g, osdSize: os, hideChannels: hc });
        };

        inputElements.forEach(input => input.addEventListener('input', triggerLiveUpdate));
        checkboxHide.addEventListener('change', triggerLiveUpdate);

        // Gestion de l'incrémentation numérique via la molette de la souris sur les inputs
        const wheelInputHandler = (e) => {
            e.stopPropagation();
            e.preventDefault();
            const input = e.target;
            let val = parseInt(input.value, 10) || 0;
            val = e.deltaY < 0 ? val + 1 : val - 1;
            input.value = clamp(val, parseInt(input.min, 10), parseInt(input.max, 10));
            triggerLiveUpdate();
        };

        inputElements.forEach(input => input.addEventListener('wheel', wheelInputHandler, { passive: false }));

        // Nettoyage des écouteurs et suppression de l'affichage modal
        const close = () => {
            document.removeEventListener('keydown', handleKeydown);
            inputElements.forEach(input => {
                input.removeEventListener('wheel', wheelInputHandler);
                input.removeEventListener('input', triggerLiveUpdate);
            });
            checkboxHide.removeEventListener('change', triggerLiveUpdate);
            overlay.remove();
        };

        // Restauration de l'état d'origine de l'interface lors d'une annulation
        const cancel = () => {
            applyLiveStyle(initialCols, initialGap, initialOsdSize, initialHideChannels);
            syncChannel.postMessage({ type: 'preview', nbreCol: initialCols, gapX: initialGap, osdSize: initialOsdSize, hideChannels: initialHideChannels });
            close();
        };

        // Sauvegarde définitive des paramètres mis à jour dans le stockage persistant
        const save = () => {
            config.nbreCol = clamp(parseInt(inputCol.value, 10) || DEFAULTS.nbreCol, LIMITS.nbreCol.min, LIMITS.nbreCol.max);
            config.gapX = clamp(parseInt(inputGap.value, 10) || DEFAULTS.gapX, LIMITS.gapX.min, LIMITS.gapX.max);
            config.osdSize = clamp(parseInt(inputOsd.value, 10) || DEFAULTS.osdSize, LIMITS.osdSize.min, LIMITS.osdSize.max);
            config.hideChannels = checkboxHide.checked;

            GM_setValue('nbreCol', config.nbreCol);
            GM_setValue('gapX', config.gapX);
            GM_setValue('osdSize', config.osdSize);
            GM_setValue('hideChannels', config.hideChannels);

            updateCSSVariables();

            const saveBtn = document.getElementById('btn-save');
            saveBtn.textContent = "Enregistré ! ✅";
            setTimeout(close, 400);
        };

        // Rétablissement de l'ensemble des configurations par défaut de l'application
        const reset = () => {
            inputCol.value = DEFAULTS.nbreCol;
            inputGap.value = DEFAULTS.gapX;
            inputOsd.value = DEFAULTS.osdSize;
            checkboxHide.checked = DEFAULTS.hideChannels;
            triggerLiveUpdate();
        };

        document.getElementById('btn-save').addEventListener('click', save);
        document.getElementById('btn-cancel').addEventListener('click', cancel);
        document.getElementById('btn-reset').addEventListener('click', reset);

        // Préservation de l'édition textuelle native et isolation de la navigation par flèches aux éléments non textuels
        function handleKeydown(e) {
            const activeElement = document.activeElement;
            const currentIndex = focusableElements.indexOf(activeElement);
            const isInput = activeElement.classList.contains('gm-popup-input');

            if (e.altKey) {
                const altKey = e.key.toLowerCase();
                if (altKey === 'e') {
                    e.preventDefault();
                    save();
                    return;
                }
                if (altKey === 'a') {
                    e.preventDefault();
                    cancel();
                    return;
                }
                if (altKey === 'r') {
                    e.preventDefault();
                    reset();
                    return;
                }
            }

            if (e.key === 'Escape') {
                e.preventDefault();
                cancel();
                return;
            }

            if (e.key === 'Enter') {
                if (activeElement.tagName === 'BUTTON' && activeElement.id !== 'btn-save') return;
                e.preventDefault();
                save();
                return;
            }

            if (e.key === 'Tab') {
                e.preventDefault();
                let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;
                if (nextIndex < 0) nextIndex = focusableElements.length - 1;
                if (nextIndex >= focusableElements.length) nextIndex = 0;
                focusableElements[nextIndex].focus();
                return;
            }

            if (isInput) {
                if (e.key.toLowerCase() === 'o' || e.key.toLowerCase() === 'h') return;

                if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
                    e.preventDefault();
                    let val = parseInt(activeElement.value, 10) || 0;
                    val = (e.key === 'ArrowUp') ? val + 1 : val - 1;
                    activeElement.value = clamp(val, parseInt(activeElement.min, 10), parseInt(activeElement.max, 10));
                    triggerLiveUpdate();
                    return;
                }

                if (e.ctrlKey || e.metaKey || ['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Backspace', 'Delete'].includes(e.key)) {
                    return;
                }
            } else {
                if (['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(e.key)) {
                    e.preventDefault();
                    let move = (e.key === 'ArrowDown' || e.key === 'ArrowRight') ? 1 : -1;
                    let nextIndex = currentIndex + move;
                    if (nextIndex < 0) nextIndex = focusableElements.length - 1;
                    if (nextIndex >= focusableElements.length) nextIndex = 0;
                    focusableElements[nextIndex].focus();
                }
            }
        }

        document.addEventListener('keydown', handleKeydown);
    }

    GM_registerMenuCommand("⚙️ Options", createSettingsPopup);

    // Interception et traitement des commandes et raccourcis clavier globaux
    window.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable ||
            (e.target.tagName === 'INPUT' && !e.target.classList.contains('gm-popup-input') && e.target.id !== 'input-hideChannels')) {
            return;
        }

        const key = e.key.toLowerCase();
        if (key === 'o') {
            e.preventDefault();
            const existingPopup = document.getElementById('gm-settings-popup');
            if (existingPopup) {
                existingPopup.querySelector('#btn-cancel').click();
            } else {
                createSettingsPopup();
            }
            return;
        }

        if (key === 'h') {
            e.preventDefault();
            config.hideChannels = !config.hideChannels;
            GM_setValue('hideChannels', config.hideChannels);
            updateCSSVariables();

            const chkHide = document.getElementById('input-hideChannels');
            if (chkHide) chkHide.checked = config.hideChannels;
            return;
        }

        if (PLAY_URL_REGEX.test(window.location.href) && !document.getElementById('gm-settings-popup')) {
            if (e.key === 'ArrowUp') {
                e.preventDefault();
                adjustVolume(5);
            }
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                adjustVolume(-5);
            }
        }
    }, true);

    // Écoute de la communication entre onglets pour actualiser l'interface en direct
    syncChannel.onmessage = (event) => {
        if (event.data.type === 'preview') {
            const { nbreCol, gapX, osdSize, hideChannels } = event.data;
            applyLiveStyle(nbreCol, gapX, osdSize, hideChannels);

            ['nbreCol', 'gapX', 'osdSize'].forEach(key => {
                const input = document.getElementById(`input-${key}`);
                if (input) input.value = event.data[key];
            });
            const chkHide = document.getElementById('input-hideChannels');
            if (chkHide) chkHide.checked = hideChannels;
        }
    };

    // Synchronisation automatique suite à un changement de données provenant d'un autre onglet
    ['nbreCol', 'gapX', 'osdSize', 'hideChannels'].forEach(key => {
        GM_addValueChangeListener(key, (name, oldVal, newVal, remote) => {
            if (remote) {
                config[name] = newVal;
                updateCSSVariables();

                if (name === 'hideChannels') {
                    const chkHide = document.getElementById('input-hideChannels');
                    if (chkHide) chkHide.checked = newVal;
                } else {
                    const input = document.getElementById(`input-${name}`);
                    if (input) input.value = newVal;
                }
            }
        });
    });

    // Initialisation globale de l'interface en fonction de l'état de chargement du document
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', updateCSSVariables);
    } else {
        updateCSSVariables();
    }
})();