AccesSight

Suite d'accessibilité complète avec loupe et synthèse vocale améliorée

当前为 2025-03-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AccesSight
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.1
  5. // @description Suite d'accessibilité complète avec loupe et synthèse vocale améliorée
  6. // @author Yglsan
  7. // @include *
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addStyle
  11. // @license GPL-3.0
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Configuration par défaut
  18. const configurationParDefaut = {
  19. tailleTexte: 16,
  20. modeContraste: 'normal',
  21. vitesseLecture: 1,
  22. surbrillanceLiens: true,
  23. modeSombre: false,
  24. loupe: {
  25. activee: false,
  26. zoom: 2,
  27. suivreCurseur: true,
  28. tailleLoupe: 200
  29. },
  30. syntheseVocale: {
  31. lireLiens: true,
  32. ignorerContenuCache: true,
  33. navigationStructurelle: true
  34. }
  35. };
  36.  
  37. // État courant
  38. let configurationActuelle = {
  39. ...configurationParDefaut,
  40. ...GM_getValue('configurationAccesSight', {})
  41. };
  42.  
  43. // Création de l'interface utilisateur principale
  44. function creerPanneauControle() {
  45. const panneau = document.createElement('div');
  46. panneau.id = 'panneau-accesight';
  47. panneau.style.cssText = `
  48. position: fixed;
  49. top: ${configurationActuelle.positionSauvegardee.y}px;
  50. left: ${configurationActuelle.positionSauvegardee.x}px;
  51. background: ${configurationActuelle.modeSombre ? '#333' : '#FFF'};
  52. color: ${configurationActuelle.modeSombre ? '#FFF' : '#000'};
  53. padding: 1rem;
  54. border-radius: 8px;
  55. box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  56. z-index: 10000;
  57. font-family: Arial, sans-serif;
  58. min-width: 300px;
  59. cursor: move;
  60. user-select: none;
  61. `;
  62.  
  63. // En-tête du panneau
  64. const enTete = document.createElement('div');
  65. enTete.innerHTML = `
  66. <h2 style="margin: 0 0 1rem 0; font-size: 1.2rem;">AccesSight Pro+</h2>
  67. <button aria-label="Fermer le panneau"
  68. style="position: absolute; top: 5px; right: 5px;
  69. background: none; border: none; cursor: pointer; color: inherit;">
  70. ×
  71. </button>
  72. `;
  73. panneau.appendChild(enTete);
  74.  
  75. // Contrôles principaux
  76. const controles = [
  77. {
  78. type: 'curseur',
  79. libelle: 'Taille du texte',
  80. min: 12,
  81. max: 36,
  82. valeur: configurationActuelle.tailleTexte,
  83. propriete: 'tailleTexte',
  84. pas: 1
  85. },
  86. {
  87. type: 'selection',
  88. libelle: 'Mode de contraste',
  89. options: ['Normal', 'Élevé', 'Inversé'],
  90. valeur: configurationActuelle.modeContraste,
  91. propriete: 'modeContraste'
  92. },
  93. {
  94. type: 'bouton',
  95. libelle: 'Lire le contenu',
  96. action: 'demarrerLectureVocale'
  97. },
  98. {
  99. type: 'interrupteur',
  100. libelle: 'Mode sombre',
  101. valeur: configurationActuelle.modeSombre,
  102. propriete: 'modeSombre'
  103. },
  104. {
  105. type: 'interrupteur',
  106. libelle: 'Activer la loupe',
  107. valeur: configurationActuelle.loupe.activee,
  108. propriete: 'loupe.activee'
  109. }
  110. ];
  111.  
  112. controles.forEach(controle => {
  113. const groupeControle = document.createElement('div');
  114. groupeControle.style.marginBottom = '1rem';
  115.  
  116. switch(controle.type) {
  117. case 'curseur':
  118. groupeControle.innerHTML = `
  119. <label style="display: block; margin-bottom: 0.5rem;">
  120. ${controle.libelle}:
  121. <output style="display: inline-block; width: 3em;">${controle.valeur}px</output>
  122. </label>
  123. <input type="range"
  124. min="${controle.min}"
  125. max="${controle.max}"
  126. step="${controle.pas}"
  127. value="${controle.valeur}"
  128. style="width: 100%;"
  129. data-propriete="${controle.propriete}">
  130. `;
  131. break;
  132.  
  133. case 'selection':
  134. groupeControle.innerHTML = `
  135. <label style="display: block; margin-bottom: 0.5rem;">${controle.libelle}</label>
  136. <select style="width: 100%; padding: 0.25rem;" data-propriete="${controle.propriete}">
  137. ${controle.options.map(option => `
  138. <option value="${option.toLowerCase()}" ${option.toLowerCase() === controle.valeur ? 'selected' : ''}>
  139. ${option}
  140. </option>
  141. `).join('')}
  142. </select>
  143. `;
  144. break;
  145.  
  146. case 'interrupteur':
  147. groupeControle.innerHTML = `
  148. <label style="display: flex; align-items: center; gap: 0.75rem;">
  149. <input type="checkbox"
  150. ${controle.valeur ? 'checked' : ''}
  151. data-propriete="${controle.propriete}"
  152. style="margin: 0;">
  153. ${controle.libelle}
  154. </label>
  155. `;
  156. break;
  157.  
  158. case 'bouton':
  159. groupeControle.innerHTML = `
  160. <button style="width: 100%; padding: 0.75rem; background: #007bff; color: white; border: none; border-radius: 4px;"
  161. data-action="${controle.action}">
  162. ${controle.libelle}
  163. </button>
  164. `;
  165. break;
  166. }
  167.  
  168. panneau.appendChild(groupeControle);
  169. });
  170.  
  171. // Gestion des événements
  172. panneau.querySelectorAll('input, select').forEach(element => {
  173. element.addEventListener('input', gererChangementConfiguration);
  174. element.addEventListener('mousedown', arreterPropagationEvenement);
  175. element.addEventListener('touchstart', arreterPropagationEvenement);
  176. });
  177.  
  178. panneau.querySelector('[data-action="demarrerLectureVocale"]').addEventListener('click', basculerLectureVocale);
  179.  
  180. // Gestion du déplacement du panneau
  181. let deplacementActif = false;
  182. let positionInitialeX = 0;
  183. let positionInitialeY = 0;
  184.  
  185. panneau.addEventListener('mousedown', commencerDeplacement);
  186. document.addEventListener('mousemove', deplacerPanneau);
  187. document.addEventListener('mouseup', arreterDeplacement);
  188.  
  189. function commencerDeplacement(evenement) {
  190. if (evenement.target.tagName === 'INPUT' || evenement.target.tagName === 'SELECT') return;
  191. deplacementActif = true;
  192. positionInitialeX = evenement.clientX - panneau.offsetLeft;
  193. positionInitialeY = evenement.clientY - panneau.offsetTop;
  194. }
  195.  
  196. function deplacerPanneau(evenement) {
  197. if (deplacementActif) {
  198. evenement.preventDefault();
  199. panneau.style.left = `${evenement.clientX - positionInitialeX}px`;
  200. panneau.style.top = `${evenement.clientY - positionInitialeY}px`;
  201. }
  202. }
  203.  
  204. function arreterDeplacement() {
  205. deplacementActif = false;
  206. configurationActuelle.positionSauvegardee = {
  207. x: parseInt(panneau.style.left),
  208. y: parseInt(panneau.style.top)
  209. };
  210. GM_setValue('configurationAccesSight', configurationActuelle);
  211. }
  212.  
  213. document.body.appendChild(panneau);
  214. appliquerParametresAccessibilite();
  215. }
  216.  
  217. // Application des paramètres d'accessibilité
  218. function appliquerParametresAccessibilite() {
  219. // Taille du texte
  220. document.documentElement.style.fontSize = `${configurationActuelle.tailleTexte}px`;
  221.  
  222. // Contraste
  223. GM_addStyle(`
  224. body {
  225. filter: ${configurationActuelle.modeContraste === 'élevé' ? 'contrast(150%)' :
  226. configurationActuelle.modeContraste === 'inversé' ? 'invert(1) hue-rotate(180deg)' : 'none'};
  227. }
  228. `);
  229.  
  230. // Mode sombre
  231. if (configurationActuelle.modeSombre) {
  232. GM_addStyle(`
  233. body {
  234. background-color: #1a1a1a !important;
  235. color: #ffffff !important;
  236. }
  237. `);
  238. }
  239.  
  240. // Surbrillance des liens
  241. if (configurationActuelle.surbrillanceLiens) {
  242. GM_addStyle(`
  243. a {
  244. outline: 2px solid #ff0000 !important;
  245. padding: 2px !important;
  246. }
  247. `);
  248. }
  249.  
  250. // Gestion de la loupe
  251. if (configurationActuelle.loupe.activee && !document.getElementById('loupe-accesight')) {
  252. creerLoupe();
  253. }
  254. }
  255.  
  256. // Création de la loupe
  257. function creerLoupe() {
  258. const loupe = document.createElement('div');
  259. loupe.id = 'loupe-accesight';
  260. loupe.style.cssText = `
  261. position: absolute;
  262. width: ${configurationActuelle.loupe.tailleLoupe}px;
  263. height: ${configurationActuelle.loupe.tailleLoupe}px;
  264. border: 2px solid #ff0000;
  265. border-radius: 50%;
  266. overflow: hidden;
  267. pointer-events: none;
  268. display: none;
  269. z-index: 100000;
  270. background: white;
  271. box-shadow: 0 0 20px rgba(0,0,0,0.3);
  272. `;
  273.  
  274. const contenuLoupe = document.createElement('div');
  275. contenuLoupe.style.cssText = `
  276. transform-origin: 0 0;
  277. will-change: transform;
  278. width: ${document.documentElement.offsetWidth}px;
  279. height: ${document.documentElement.offsetHeight}px;
  280. `;
  281.  
  282. loupe.appendChild(contenuLoupe);
  283. document.body.appendChild(loupe);
  284.  
  285. document.addEventListener('mousemove', evenement => {
  286. if (!configurationActuelle.loupe.activee) return;
  287.  
  288. const positionX = evenement.clientX;
  289. const positionY = evenement.clientY;
  290. loupe.style.display = 'block';
  291. loupe.style.left = `${positionX + 20}px`;
  292. loupe.style.top = `${positionY + 20}px`;
  293.  
  294. const zoom = configurationActuelle.loupe.zoom;
  295. contenuLoupe.style.transform = `
  296. translate(${-positionX * zoom + configurationActuelle.loupe.tailleLoupe/2}px,
  297. ${-positionY * zoom + configurationActuelle.loupe.tailleLoupe/2}px)
  298. scale(${zoom})
  299. `;
  300. contenuLoupe.innerHTML = document.documentElement.cloneNode(true);
  301. });
  302. }
  303.  
  304. // Gestion de la synthèse vocale
  305. let instanceLectureVocale = null;
  306. function basculerLectureVocale() {
  307. if (!instanceLectureVocale) {
  308. demarrerLectureVocale();
  309. } else {
  310. arreterLectureVocale();
  311. }
  312. }
  313.  
  314. function demarrerLectureVocale() {
  315. const contenuStructure = extraireContenuStructure();
  316. instanceLectureVocale = new SpeechSynthesisUtterance(contenuStructure);
  317. instanceLectureVocale.rate = configurationActuelle.vitesseLecture;
  318. instanceLectureVocale.onboundary = evenement => surlignerMotCourant(evenement);
  319. instanceLectureVocale.onend = () => reinitialiserSurlignage();
  320. window.speechSynthesis.speak(instanceLectureVocale);
  321. }
  322.  
  323. function extraireContenuStructure() {
  324. const elementsVisibles = Array.from(document.body.querySelectorAll(
  325. 'h1, h2, h3, h4, h5, h6, p, li, caption, figcaption, blockquote'
  326. )).filter(element => {
  327. return configurationActuelle.syntheseVocale.ignorerContenuCache ?
  328. element.offsetParent !== null : true;
  329. });
  330.  
  331. return elementsVisibles.map(element => {
  332. const typeElement = element.tagName.toLowerCase();
  333. return `${typeElement} : ${element.textContent}`;
  334. }).join('. ');
  335. }
  336.  
  337. function surlignerMotCourant(evenement) {
  338. const indexCaractere = evenement.charIndex;
  339. const texteComplet = instanceLectureVocale.text;
  340. const mots = texteComplet.split(/\s+/);
  341. let indexAccumule = 0;
  342. let motCourant = '';
  343. for (const mot of mots) {
  344. indexAccumule += mot.length + 1;
  345. if (indexAccumule > indexCaractere) {
  346. motCourant = mot;
  347. break;
  348. }
  349. }
  350.  
  351. const elements = document.querySelectorAll('*:not(script):not(style)');
  352. elements.forEach(element => {
  353. element.childNodes.forEach(node => {
  354. if (node.nodeType === Node.TEXT_NODE && node.textContent.includes(motCourant)) {
  355. const span = document.createElement('span');
  356. span.style.backgroundColor = 'yellow';
  357. span.style.color = 'black';
  358. span.textContent = motCourant;
  359. const nouveauContenu = node.textContent.replace(
  360. new RegExp(motCourant, 'g'),
  361. span.outerHTML
  362. );
  363. const nouveauNoeud = document.createRange().createContextualFragment(nouveauContenu);
  364. node.replaceWith(...nouveauNoeud.childNodes);
  365. }
  366. });
  367. });
  368. }
  369.  
  370. function reinitialiserSurlignage() {
  371. document.querySelectorAll('span[style*="yellow"]').forEach(span => {
  372. const parent = span.parentNode;
  373. parent.replaceChild(document.createTextNode(span.textContent), span);
  374. });
  375. instanceLectureVocale = null;
  376. }
  377.  
  378. // Gestion des changements de configuration
  379. function gererChangementConfiguration(evenement) {
  380. const propriete = evenement.target.dataset.propriete;
  381. let valeur = evenement.target.type === 'checkbox' ?
  382. evenement.target.checked :
  383. evenement.target.value;
  384.  
  385. if (propriete === 'tailleTexte') valeur = parseInt(valeur);
  386. if (propriete.includes('.')) {
  387. const [parent, enfant] = propriete.split('.');
  388. configurationActuelle[parent][enfant] = valeur;
  389. } else {
  390. configurationActuelle[propriete] = valeur;
  391. }
  392.  
  393. GM_setValue('configurationAccesSight', configurationActuelle);
  394. appliquerParametresAccessibilite();
  395.  
  396. if (propriete === 'tailleTexte') {
  397. evenement.target.parentNode.querySelector('output').textContent = `${valeur}px`;
  398. }
  399. }
  400.  
  401. // Fonctions utilitaires
  402. function arreterPropagationEvenement(evenement) {
  403. evenement.stopPropagation();
  404. }
  405.  
  406. // Initialisation
  407. function initialiser() {
  408. if (!document.getElementById('panneau-accesight')) {
  409. creerPanneauControle();
  410. }
  411. }
  412.  
  413. // Démarrage
  414. if (document.readyState === 'loading') {
  415. document.addEventListener('DOMContentLoaded', initialiser);
  416. } else {
  417. initialiser();
  418. }
  419. })();