// ==UserScript==
// @name AccesSight
// @namespace http://tampermonkey.net/
// @version 3.1
// @description Suite d'accessibilité complète avec loupe et synthèse vocale améliorée
// @author Yglsan
// @include *
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @license GPL-3.0
// ==/UserScript==
(function() {
'use strict';
// Configuration par défaut
const configurationParDefaut = {
tailleTexte: 16,
modeContraste: 'normal',
vitesseLecture: 1,
surbrillanceLiens: true,
modeSombre: false,
loupe: {
activee: false,
zoom: 2,
suivreCurseur: true,
tailleLoupe: 200
},
syntheseVocale: {
lireLiens: true,
ignorerContenuCache: true,
navigationStructurelle: true
}
};
// État courant
let configurationActuelle = {
...configurationParDefaut,
...GM_getValue('configurationAccesSight', {})
};
// Création de l'interface utilisateur principale
function creerPanneauControle() {
const panneau = document.createElement('div');
panneau.id = 'panneau-accesight';
panneau.style.cssText = `
position: fixed;
top: ${configurationActuelle.positionSauvegardee.y}px;
left: ${configurationActuelle.positionSauvegardee.x}px;
background: ${configurationActuelle.modeSombre ? '#333' : '#FFF'};
color: ${configurationActuelle.modeSombre ? '#FFF' : '#000'};
padding: 1rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 10000;
font-family: Arial, sans-serif;
min-width: 300px;
cursor: move;
user-select: none;
`;
// En-tête du panneau
const enTete = document.createElement('div');
enTete.innerHTML = `
<h2 style="margin: 0 0 1rem 0; font-size: 1.2rem;">AccesSight Pro+</h2>
<button aria-label="Fermer le panneau"
style="position: absolute; top: 5px; right: 5px;
background: none; border: none; cursor: pointer; color: inherit;">
×
</button>
`;
panneau.appendChild(enTete);
// Contrôles principaux
const controles = [
{
type: 'curseur',
libelle: 'Taille du texte',
min: 12,
max: 36,
valeur: configurationActuelle.tailleTexte,
propriete: 'tailleTexte',
pas: 1
},
{
type: 'selection',
libelle: 'Mode de contraste',
options: ['Normal', 'Élevé', 'Inversé'],
valeur: configurationActuelle.modeContraste,
propriete: 'modeContraste'
},
{
type: 'bouton',
libelle: 'Lire le contenu',
action: 'demarrerLectureVocale'
},
{
type: 'interrupteur',
libelle: 'Mode sombre',
valeur: configurationActuelle.modeSombre,
propriete: 'modeSombre'
},
{
type: 'interrupteur',
libelle: 'Activer la loupe',
valeur: configurationActuelle.loupe.activee,
propriete: 'loupe.activee'
}
];
controles.forEach(controle => {
const groupeControle = document.createElement('div');
groupeControle.style.marginBottom = '1rem';
switch(controle.type) {
case 'curseur':
groupeControle.innerHTML = `
<label style="display: block; margin-bottom: 0.5rem;">
${controle.libelle}:
<output style="display: inline-block; width: 3em;">${controle.valeur}px</output>
</label>
<input type="range"
min="${controle.min}"
max="${controle.max}"
step="${controle.pas}"
value="${controle.valeur}"
style="width: 100%;"
data-propriete="${controle.propriete}">
`;
break;
case 'selection':
groupeControle.innerHTML = `
<label style="display: block; margin-bottom: 0.5rem;">${controle.libelle}</label>
<select style="width: 100%; padding: 0.25rem;" data-propriete="${controle.propriete}">
${controle.options.map(option => `
<option value="${option.toLowerCase()}" ${option.toLowerCase() === controle.valeur ? 'selected' : ''}>
${option}
</option>
`).join('')}
</select>
`;
break;
case 'interrupteur':
groupeControle.innerHTML = `
<label style="display: flex; align-items: center; gap: 0.75rem;">
<input type="checkbox"
${controle.valeur ? 'checked' : ''}
data-propriete="${controle.propriete}"
style="margin: 0;">
${controle.libelle}
</label>
`;
break;
case 'bouton':
groupeControle.innerHTML = `
<button style="width: 100%; padding: 0.75rem; background: #007bff; color: white; border: none; border-radius: 4px;"
data-action="${controle.action}">
${controle.libelle}
</button>
`;
break;
}
panneau.appendChild(groupeControle);
});
// Gestion des événements
panneau.querySelectorAll('input, select').forEach(element => {
element.addEventListener('input', gererChangementConfiguration);
element.addEventListener('mousedown', arreterPropagationEvenement);
element.addEventListener('touchstart', arreterPropagationEvenement);
});
panneau.querySelector('[data-action="demarrerLectureVocale"]').addEventListener('click', basculerLectureVocale);
// Gestion du déplacement du panneau
let deplacementActif = false;
let positionInitialeX = 0;
let positionInitialeY = 0;
panneau.addEventListener('mousedown', commencerDeplacement);
document.addEventListener('mousemove', deplacerPanneau);
document.addEventListener('mouseup', arreterDeplacement);
function commencerDeplacement(evenement) {
if (evenement.target.tagName === 'INPUT' || evenement.target.tagName === 'SELECT') return;
deplacementActif = true;
positionInitialeX = evenement.clientX - panneau.offsetLeft;
positionInitialeY = evenement.clientY - panneau.offsetTop;
}
function deplacerPanneau(evenement) {
if (deplacementActif) {
evenement.preventDefault();
panneau.style.left = `${evenement.clientX - positionInitialeX}px`;
panneau.style.top = `${evenement.clientY - positionInitialeY}px`;
}
}
function arreterDeplacement() {
deplacementActif = false;
configurationActuelle.positionSauvegardee = {
x: parseInt(panneau.style.left),
y: parseInt(panneau.style.top)
};
GM_setValue('configurationAccesSight', configurationActuelle);
}
document.body.appendChild(panneau);
appliquerParametresAccessibilite();
}
// Application des paramètres d'accessibilité
function appliquerParametresAccessibilite() {
// Taille du texte
document.documentElement.style.fontSize = `${configurationActuelle.tailleTexte}px`;
// Contraste
GM_addStyle(`
body {
filter: ${configurationActuelle.modeContraste === 'élevé' ? 'contrast(150%)' :
configurationActuelle.modeContraste === 'inversé' ? 'invert(1) hue-rotate(180deg)' : 'none'};
}
`);
// Mode sombre
if (configurationActuelle.modeSombre) {
GM_addStyle(`
body {
background-color: #1a1a1a !important;
color: #ffffff !important;
}
`);
}
// Surbrillance des liens
if (configurationActuelle.surbrillanceLiens) {
GM_addStyle(`
a {
outline: 2px solid #ff0000 !important;
padding: 2px !important;
}
`);
}
// Gestion de la loupe
if (configurationActuelle.loupe.activee && !document.getElementById('loupe-accesight')) {
creerLoupe();
}
}
// Création de la loupe
function creerLoupe() {
const loupe = document.createElement('div');
loupe.id = 'loupe-accesight';
loupe.style.cssText = `
position: absolute;
width: ${configurationActuelle.loupe.tailleLoupe}px;
height: ${configurationActuelle.loupe.tailleLoupe}px;
border: 2px solid #ff0000;
border-radius: 50%;
overflow: hidden;
pointer-events: none;
display: none;
z-index: 100000;
background: white;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
`;
const contenuLoupe = document.createElement('div');
contenuLoupe.style.cssText = `
transform-origin: 0 0;
will-change: transform;
width: ${document.documentElement.offsetWidth}px;
height: ${document.documentElement.offsetHeight}px;
`;
loupe.appendChild(contenuLoupe);
document.body.appendChild(loupe);
document.addEventListener('mousemove', evenement => {
if (!configurationActuelle.loupe.activee) return;
const positionX = evenement.clientX;
const positionY = evenement.clientY;
loupe.style.display = 'block';
loupe.style.left = `${positionX + 20}px`;
loupe.style.top = `${positionY + 20}px`;
const zoom = configurationActuelle.loupe.zoom;
contenuLoupe.style.transform = `
translate(${-positionX * zoom + configurationActuelle.loupe.tailleLoupe/2}px,
${-positionY * zoom + configurationActuelle.loupe.tailleLoupe/2}px)
scale(${zoom})
`;
contenuLoupe.innerHTML = document.documentElement.cloneNode(true);
});
}
// Gestion de la synthèse vocale
let instanceLectureVocale = null;
function basculerLectureVocale() {
if (!instanceLectureVocale) {
demarrerLectureVocale();
} else {
arreterLectureVocale();
}
}
function demarrerLectureVocale() {
const contenuStructure = extraireContenuStructure();
instanceLectureVocale = new SpeechSynthesisUtterance(contenuStructure);
instanceLectureVocale.rate = configurationActuelle.vitesseLecture;
instanceLectureVocale.onboundary = evenement => surlignerMotCourant(evenement);
instanceLectureVocale.onend = () => reinitialiserSurlignage();
window.speechSynthesis.speak(instanceLectureVocale);
}
function extraireContenuStructure() {
const elementsVisibles = Array.from(document.body.querySelectorAll(
'h1, h2, h3, h4, h5, h6, p, li, caption, figcaption, blockquote'
)).filter(element => {
return configurationActuelle.syntheseVocale.ignorerContenuCache ?
element.offsetParent !== null : true;
});
return elementsVisibles.map(element => {
const typeElement = element.tagName.toLowerCase();
return `${typeElement} : ${element.textContent}`;
}).join('. ');
}
function surlignerMotCourant(evenement) {
const indexCaractere = evenement.charIndex;
const texteComplet = instanceLectureVocale.text;
const mots = texteComplet.split(/\s+/);
let indexAccumule = 0;
let motCourant = '';
for (const mot of mots) {
indexAccumule += mot.length + 1;
if (indexAccumule > indexCaractere) {
motCourant = mot;
break;
}
}
const elements = document.querySelectorAll('*:not(script):not(style)');
elements.forEach(element => {
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes(motCourant)) {
const span = document.createElement('span');
span.style.backgroundColor = 'yellow';
span.style.color = 'black';
span.textContent = motCourant;
const nouveauContenu = node.textContent.replace(
new RegExp(motCourant, 'g'),
span.outerHTML
);
const nouveauNoeud = document.createRange().createContextualFragment(nouveauContenu);
node.replaceWith(...nouveauNoeud.childNodes);
}
});
});
}
function reinitialiserSurlignage() {
document.querySelectorAll('span[style*="yellow"]').forEach(span => {
const parent = span.parentNode;
parent.replaceChild(document.createTextNode(span.textContent), span);
});
instanceLectureVocale = null;
}
// Gestion des changements de configuration
function gererChangementConfiguration(evenement) {
const propriete = evenement.target.dataset.propriete;
let valeur = evenement.target.type === 'checkbox' ?
evenement.target.checked :
evenement.target.value;
if (propriete === 'tailleTexte') valeur = parseInt(valeur);
if (propriete.includes('.')) {
const [parent, enfant] = propriete.split('.');
configurationActuelle[parent][enfant] = valeur;
} else {
configurationActuelle[propriete] = valeur;
}
GM_setValue('configurationAccesSight', configurationActuelle);
appliquerParametresAccessibilite();
if (propriete === 'tailleTexte') {
evenement.target.parentNode.querySelector('output').textContent = `${valeur}px`;
}
}
// Fonctions utilitaires
function arreterPropagationEvenement(evenement) {
evenement.stopPropagation();
}
// Initialisation
function initialiser() {
if (!document.getElementById('panneau-accesight')) {
creerPanneauControle();
}
}
// Démarrage
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialiser);
} else {
initialiser();
}
})();