AccesSight

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

目前為 2025-03-09 提交的版本,檢視 最新版本

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址