Greasy Fork is available in English.
Ajuste la grille de Orange TV, gère le volume au clavier/souris, cache les chaînes non souscrites et offre des raccourcis.
// ==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();
}
})();