MDK Links

Sélectionner, copier ou ouvrir des liens.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         MDK Links
// @namespace    MDK Scripts
// @version      7.1
// @description  Sélectionner, copier ou ouvrir des liens.
// @author       MDK
// @license      MIT
// @match        *://*/*
// @icon         data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'%3E%3C/path%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'%3E%3C/path%3E%3C/svg%3E
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // CONFIGURATION ET ÉTAT GLOBAL
    // ==========================================
    const MAX_TABS_SECURITY = 25; // Limite de sécurité pour éviter le crash du navigateur
    const State = {
        isDragging: false, shouldBlockContextMenu: false, startedWithRightClick: false, lastLassoEndTime: 0,
        actionType: null, startPageX: 0, startPageY: 0, clientMouseX: 0, clientMouseY: 0,
        selectedLinks: [], cachedLinksGeometry: [], keyupTimeout: null, liveForeground: false,
        rafId: null, hoveredLink: null, toastTimeout: null
    };

    // ==========================================
    // GESTION DES THÈMES ET RENDU DES LIENS
    // ==========================================
    const StylesManager = {
        catalog: {
            'starwars': { name: '🚀 Star Wars (Galactique)', css: `.lasso-selected { background: #000000 !important; color: #ffe81f !important; font-family: "Impact", "Arial Black", sans-serif !important; font-weight: bold !important; text-transform: uppercase !important; letter-spacing: 2px !important; border: 2px solid #ffe81f !important; padding: 2px 6px; text-shadow: 0 0 4px rgba(255, 232, 31, 0.6) !important; box-shadow: 0 0 10px rgba(0,0,0,0.8) !important; }` },
            'stranger': { name: '🩸 Stranger Things (Rétro 80s)', css: `.lasso-selected { background: #0c0202 !important; color: #f11313 !important; font-family: "Bookman Old Style", "Georgia", serif !important; font-weight: 900 !important; text-shadow: 0 0 6px #ff0000, 0 0 15px #8b0000 !important; letter-spacing: 1px !important; border-top: 2px solid #f11313 !important; border-bottom: 2px solid #f11313 !important; padding: 3px 6px; }` },
            'breaking': { name: '🧪 Breaking Bad (Alchimie)', css: `.lasso-selected { background: #0f381f !important; color: #ffffff !important; font-family: "Arial", "Helvetica", sans-serif !important; font-weight: bold !important; border: 2px solid #4ba74a !important; padding: 2px 8px; box-shadow: 0 0 12px rgba(75, 167, 74, 0.7), inset 0 0 6px rgba(0,0,0,0.5) !important; border-radius: 0px !important; }` },
            'fallout': { name: '☢️ Fallout Pip-Boy', css: `.lasso-selected { background: #001100 !important; color: #33ff33 !important; font-family: "Courier New", monospace !important; font-weight: bold !important; border: 2px solid #33ff33 !important; padding: 2px 4px; text-shadow: 0 0 6px #33ff33 !important; box-shadow: inset 0 0 8px #003300, 0 0 8px rgba(51,255,51,0.5) !important; letter-spacing: 1px; }` },
            'cyberpunk': { name: '🤖 Cyberpunk 2077', css: `.lasso-selected { background: #fcee0a !important; color: #000000 !important; font-family: "Arial Black", sans-serif !important; font-weight: 900 !important; text-transform: uppercase; border-left: 4px solid #00f0ff !important; padding: 2px 6px; box-shadow: 3px 3px 0px #00f0ff !important; transform: skewX(-5deg); display: inline-block; }` },
            'chiaroscuro': { name: '🌓 Clair-Obscur (Art)', css: `.lasso-selected { background: #0a0806 !important; color: #ffeedd !important; border: 1px solid #9e7844 !important; box-shadow: 0 0 15px rgba(255,140,0,0.25), inset 0 0 12px rgba(0,0,0,0.9) !important; text-shadow: 0 0 3px rgba(255,238,221,0.5) !important; font-style: italic !important; font-family: "Georgia", serif !important; padding: 2px 6px; }` },
            'matrix': { name: '📟 Code Matrix (Vert)', css: `.lasso-selected { background: #000000 !important; color: #00ff00 !important; font-family: monospace !important; font-weight: bold !important; border: 1px solid #00ff00 !important; padding: 2px; text-shadow: 0 0 3px #00ff00 !important; }` },
            'amber': { name: '🍂 Terminal Ambre (Retro)', css: `.lasso-selected { background: #110800 !important; color: #ffb000 !important; font-family: monospace !important; font-weight: bold !important; border: 1px solid #ffb000 !important; padding: 2px; text-shadow: 0 0 4px #ffb000 !important; }` },
            'synthwave': { name: '🌆 Cyberpunk / Synthwave', css: `.lasso-selected { background: rgba(43, 0, 71, 0.85) !important; color: #00ffff !important; border: 2px solid #ff007f !important; box-shadow: 0 0 10px #ff007f, inset 0 0 5px #00ffff !important; text-shadow: 0 0 3px #00ffff !important; border-radius: 4px; }` },
            'magma': { name: '🔥 Magma Enflammé', css: `.lasso-selected { border-radius: 4px; box-shadow: 0 0 12px #ff4500, inset 0 0 6px #ff8c00 !important; background: rgba(30, 5, 0, 0.95) !important; color: #ffcc00 !important; font-weight: bold !important; text-shadow: 0 0 2px #ff4500; }` },
            'blueprint': { name: '📐 Plan Bleu (Blueprint)', css: `.lasso-selected { background: #0033aa !important; color: #ffffff !important; outline: 2px dashed #ffffff !important; outline-offset: -1px; background-image: linear-gradient(rgba(255,255,255,.15) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.15) 1px, transparent 1px) !important; background-size: 8px 8px !important; font-family: sans-serif; }` },
            'halo': { name: '🔵 Halo Électrique', css: `.lasso-selected { box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.6) !important; border-radius: 2px; }` },
            'highlighter': { name: '🟡 Surligneur Jaune', css: `.lasso-selected { background: #fff59d !important; color: #000000 !important; padding: 2px 4px; border-radius: 3px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }` },
            'custom': { name: '🛠️ Personnalisé (CSS)', css: '' }
        },
        element: null,
        init() {
            this.element = document.createElement('style');
            this.element.id = 'lasso-dynamic-style';
            document.head.appendChild(this.element);
            this.apply(GM_getValue('selectedStyle', 'starwars'));
        },
        apply(key, customCSS) {
            this.element.textContent = this.catalog[key]?.css || customCSS || GM_getValue('customCSS', '');
        }
    };

    // Injection des styles CSS de l'interface (Modale de configuration, Lasso de sélection et Toasts)
    GM_addStyle(`
        :root { --popup-bg: #ffffff; --popup-text: #333333; --popup-border: #cccccc; --popup-input-bg: #ffffff; --focus-outline: #007bff; }
        @media (prefers-color-scheme: dark) { :root { --popup-bg: #1f1f1f; --popup-text: #f0f0f0; --popup-border: #444444; --popup-input-bg: #2d2d2d; } }
        .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.15s ease-out; }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        .gm-popup { background-color: var(--popup-bg); color: var(--popup-text); padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.4); width: 500px; 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: 10px; width: 100%; margin-bottom: 2px; }
        .gm-popup-icon { display: flex; align-items: center; justify-content: center; color: var(--focus-outline); }
        .gm-popup-title { margin: 0; font-size: 19px; font-weight: 800; text-align: left; letter-spacing: 0.5px; }
        .gm-popup-row { display: flex; justify-content: space-between; align-items: center; width: 100%; }
        .gm-popup-footer { display: flex; justify-content: space-between; margin-top: 4px; gap: 10px; width: 100%; }
        .gm-popup-input { width: 180px; padding: 6px 10px; background: var(--popup-input-bg); color: var(--popup-text); border: 1px solid var(--popup-border); border-radius: 4px; box-sizing: border-box; font-size: 14px; }
        .gm-popup-input:focus, .gm-btn-action:focus { outline: 2px solid var(--focus-outline) !important; outline-offset: 0px; }
        .gm-key-container { display: flex; gap: 4px; }
        .gm-key-pill { position: relative; display: inline-flex; align-items: center; justify-content: center; padding: 5px 8px; background: var(--popup-input-bg); color: var(--popup-text); border: 1px solid var(--popup-border); border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; min-width: 44px; user-select: none; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
        .gm-key-pill input { position: absolute; clip: rect(0, 0, 0, 0); padding: 0; border: 0; height: 1px; width: 1px; overflow: hidden; }
        .gm-key-pill.active { background: #007bff; color: #ffffff; border-color: #007bff; box-shadow: 0 2px 6px rgba(0, 123, 255, 0.4); }
        .gm-key-pill:focus-within { outline: 2px solid #ff8c00 !important; outline-offset: 1px; }
        .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: #28a745; } #btn-cancel { background: #6c757d; } #btn-reset { background: #dc3545; }
        #btn-save:hover { background: #218838; } #btn-cancel:hover { background: #5a6268; } #btn-reset:hover { background: #bd2130; }
        .gm-popup-fieldset { background: var(--popup-input-bg) !important; color: var(--popup-text) !important; border: 1px solid var(--popup-border) !important; display: flex !important; flex-direction: column; gap: 12px; padding: 16px !important; border-radius: 6px; box-sizing: border-box; width: 100%; }
        .gm-animated-toast { position: fixed; bottom: 50px; left: 50%; transform: translateX(-50%) translateY(15px); padding: 12px 24px; background-color: #28a745; color: #ffffff; border-radius: 8px; z-index: 2147483647; font-family: sans-serif; box-shadow: 0 5px 18px rgba(0,0,0,0.35); opacity: 0; pointer-events: none; transition: opacity 0.22s cubic-bezier(0.4, 0, 0.2, 1), transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); width: 500px; max-width: 90vw; box-sizing: border-box; text-align: center; }
        .gm-animated-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
        .gm-toast-title { font-size: 14px; font-weight: bold; line-height: 1.3; }
        .gm-toast-content { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; white-space: pre-wrap; word-break: break-all; font-size: 13px; font-weight: normal; line-height: 1.4; margin-top: 5px; opacity: 0.95; }
    `);

    // ==========================================
    // INTERFACE ÉLÉMENTS ET COMPOSANTS VISUELS (UI)
    // ==========================================
    const UI = {
        lasso: document.createElement('div'),
        counter: document.createElement('div'),
        toast: document.createElement('div'),
        init() {
            Object.assign(this.lasso.style, { position: 'absolute', border: '2px dashed #007bff', backgroundColor: 'rgba(0, 123, 255, 0.1)', display: 'none', pointerEvents: 'none', zIndex: '999999' });
            Object.assign(this.counter.style, { position: 'fixed', padding: '6px 12px', backgroundColor: '#222', color: '#fff', borderRadius: '5px', fontSize: '13px', fontWeight: 'bold', display: 'none', pointerEvents: 'none', zIndex: '1000000', boxShadow: '0 2px 8px rgba(0,0,0,0.4)', whiteSpace: 'nowrap', fontFamily: 'sans-serif' });
            this.toast.className = 'gm-animated-toast';
            document.body.append(this.lasso, this.counter, this.toast);
        },
        showToast(title, content = '') {
            clearTimeout(State.toastTimeout);
            this.toast.innerHTML = content ? `<div class="gm-toast-title">${title}</div><div class="gm-toast-content">${content}</div>` : `<div class="gm-toast-title">${title}</div>`;
            this.toast.classList.add('show');
            State.toastTimeout = setTimeout(() => this.toast.classList.remove('show'), 2800);
        },
        updateCounter() {
            if (!State.isDragging) return;
            if (State.actionType === 'copy') {
                this.counter.textContent = `Copier ${State.selectedLinks.length} lien${State.selectedLinks.length > 1 ? 's' : ''}`;
            } else if (State.actionType === 'open') {
                this.counter.textContent = `Ouvrir ${State.selectedLinks.length} lien${State.selectedLinks.length > 1 ? 's' : ''} [${State.liveForeground ? '👁️ Avant-plan' : '💤 Arrière-plan'}]`;
            }
        },
        playSuccessChime() { // Retour audio discret lors des validations de captures
            try {
                const AudioCtx = window.AudioContext || window.webkitAudioContext;
                if (!AudioCtx) return;
                const ctx = new AudioCtx();
                const playNote = (freq, start, dur) => {
                    const osc = ctx.createOscillator(), gain = ctx.createGain();
                    osc.type = 'sine'; osc.frequency.setValueAtTime(freq, start);
                    gain.gain.setValueAtTime(0.05, start); gain.gain.exponentialRampToValueAtTime(0.00001, start + dur);
                    osc.connect(gain); gain.connect(ctx.destination);
                    osc.start(start); osc.stop(start + dur);
                };
                const now = ctx.currentTime;
                playNote(523.25, now, 0.07); playNote(659.25, now + 0.04, 0.11);
            } catch (e) {}
        }
    };

    // Vérification unifiée et dynamique des touches modificatrices configurées
    function matchMod(e, prefix) {
        return e.ctrlKey === GM_getValue(prefix + 'Ctrl', true) &&
               e.metaKey === GM_getValue(prefix + 'Meta', prefix === 'open') &&
               e.altKey === GM_getValue(prefix + 'Alt', false) &&
               e.shiftKey === GM_getValue(prefix + 'Shift', false);
    }

    function getActionFromModifiers(e) {
        return matchMod(e, 'copy') ? 'copy' : (matchMod(e, 'open') ? 'open' : null);
    }

    // ==========================================
    // MOTEUR GÉOMÉTRIQUE DU LASSO DE SÉLECTION
    // ==========================================
    const LassoEngine = {
        cacheGeometries() { // Pré-calcul global de la position des liens du DOM pour optimiser le calcul du scroll
            const scrollX = window.scrollX, scrollY = window.scrollY;
            State.cachedLinksGeometry = Array.from(document.querySelectorAll('a[href]'))
                .filter(a => {
                    if (a.id === 'preview-link') return false;
                    const h = a.getAttribute('href').trim().toLowerCase();
                    return !h.startsWith('javascript:') && h !== '#' && h !== '';
                })
                .map(a => {
                    const r = a.getBoundingClientRect();
                    return {
                        el: a, url: a.href,
                        left: r.left + scrollX, top: r.top + scrollY,
                        right: r.right + scrollX, bottom: r.bottom + scrollY,
                        isSelected: false
                    };
                });
        },
        render() { // Calcul d'intersection de boîte englobante exécuté via RequestAnimationFrame (RAF)
            if (!State.isDragging) return;
            const cx = State.clientMouseX + window.scrollX, cy = State.clientMouseY + window.scrollY;
            const left = Math.min(cx, State.startPageX), top = Math.min(cy, State.startPageY);
            const w = Math.abs(cx - State.startPageX), h = Math.abs(cy - State.startPageY);

            Object.assign(UI.lasso.style, { left: left + 'px', top: top + 'px', width: w + 'px', height: h + 'px' });

            const urls = new Set();
            State.cachedLinksGeometry.forEach(item => {
                const inside = item.left < left + w && item.right > left && item.top < top + h && item.bottom > top;
                if (inside) {
                    urls.add(item.url);
                    if (!item.isSelected) { item.el.classList.add('lasso-selected'); item.isSelected = true; }
                } else if (item.isSelected) {
                    item.el.classList.remove('lasso-selected'); item.isSelected = false;
                }
            });

            State.selectedLinks = Array.from(urls);
            if (!State.selectedLinks.length) {
                UI.counter.style.display = 'none';
            } else {
                UI.updateCounter();
                UI.counter.style.display = 'block';
                const cw = UI.counter.offsetWidth || 120, ch = UI.counter.offsetHeight || 30, gap = 15;
                let tx = State.clientMouseX + gap, ty = State.clientMouseY + gap;
                if (tx + cw > window.innerWidth) tx = State.clientMouseX - gap - cw;
                if (ty + ch > window.innerHeight) ty = State.clientMouseY - gap - ch;
                Object.assign(UI.counter.style, { left: Math.max(5, tx) + 'px', top: Math.max(5, ty) + 'px' });
            }
            State.rafId = null;
        }
    };

    // ==========================================
    // INTERFACE GRAPHIQUE DU MENU DE CONFIGURATION
    // ==========================================
    function createSettingsUI() {
        if (document.getElementById('gm-settings-popup')) return;

        const initialStyle = GM_getValue('selectedStyle', 'starwars'), initialCustomCSS = GM_getValue('customCSS', '');
        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">
                <div class="gm-popup-icon"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg></div>
                <h3 class="gm-popup-title">MDK Links</h3>
            </div>
            <fieldset id="trigger-fieldset" class="gm-popup-fieldset">
                <div style="display: flex; flex-direction: column; gap: 4px; color: inherit;">
                    <span style="font-size: 13px; font-weight: bold; text-align: left; margin-bottom: 4px;">Copier dans le presse-papier</span>
                    <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
                        <div class="gm-key-container">
                            <label class="gm-key-pill" id="lbl-copy-ctrl"><input type="checkbox" id="chk-copy-ctrl">Ctrl</label>
                            <label class="gm-key-pill" id="lbl-copy-meta"><input type="checkbox" id="chk-copy-meta">Win</label>
                            <label class="gm-key-pill" id="lbl-copy-alt"><input type="checkbox" id="chk-copy-alt">Alt</label>
                            <label class="gm-key-pill" id="lbl-copy-shift"><input type="checkbox" id="chk-copy-shift">Shift</label>
                        </div>
                        <div style="font-weight: bold; opacity: 0.8; font-size: 14px;">+</div>
                        <select id="mouse-copy-select" class="gm-popup-input">
                            <option value="0">Clic Gauche</option><option value="1">Clic Milieu</option><option value="2">Clic Droit</option>
                        </select>
                    </div>
                </div>
                <div style="display: flex; flex-direction: column; gap: 4px; color: inherit; margin-top: 1.2em;">
                    <div style="display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 4px;">
                        <span style="font-size: 13px; font-weight: bold;">Ouvrir les liens dans un nouvel onglet</span>
                        <label id="lbl-open-foreground" class="gm-key-pill" style="font-size: 11px; padding: 2px 8px; width: 130px !important; box-sizing: border-box; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin: 0; border-radius: 4px; text-shadow: none;">
                            <input type="checkbox" id="chk-open-foreground"><span id="txt-open-foreground">Arrière-plan 💤</span>
                        </label>
                    </div>
                    <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
                        <div class="gm-key-container">
                            <label class="gm-key-pill" id="lbl-open-ctrl"><input type="checkbox" id="chk-open-ctrl">Ctrl</label>
                            <label class="gm-key-pill" id="lbl-open-meta"><input type="checkbox" id="chk-open-meta">Win</label>
                            <label class="gm-key-pill" id="lbl-open-alt"><input type="checkbox" id="chk-open-alt">Alt</label>
                            <label class="gm-key-pill" id="lbl-open-shift"><input type="checkbox" id="chk-open-shift">Shift</label>
                        </div>
                        <div style="font-weight: bold; opacity: 0.8; font-size: 14px;">+</div>
                        <select id="mouse-open-select" class="gm-popup-input">
                            <option value="0">Clic Gauche</option><option value="1">Clic Milieu</option><option value="2">Clic Droit</option>
                        </select>
                    </div>
                </div>
                <div id="trigger-error" style="display: none; color: #ff3333 !important; font-weight: 800 !important; text-align: center !important; font-size: 13px; margin-top: 2px; width: 100%; text-shadow: none !important;">
                    ⚠️ Combinaison déjà utilisée pour une autre action !
                </div>
            </fieldset>
            <div style="font-size: 11px; opacity: 0.8; line-height: 1.4; text-align: left; margin-top: -4px; margin-bottom: 2px; padding: 0 4px; font-style: italic;">
                ⚡ <strong>Astuce :</strong> Survolez un lien et faites <strong>Ctrl + C</strong> pour le copier instantanément !<br>
                💡 <strong>Bascule lasso :</strong> Modifiez vos modificateurs en cours de glisse (Copier ⇄ Ouvrir). Touche <strong>A</strong> pour intervertir (👁️ ⇄ 💤).
            </div>
            <div class="gm-popup-row">
                <label style="font-weight: bold;">Style de sélection :</label>
                <select id="style-select" class="gm-popup-input" style="width: 240px !important;">
                    ${Object.keys(StylesManager.catalog).map(k=>`<option value="${k}">${StylesManager.catalog[k].name}</option>`).join('')}
                </select>
            </div>
            <div id="preview-wrapper" style="height:60px; border:1px solid var(--popup-border); border-radius:4px; display:flex; align-items:center; justify-content:center; background:#111; overflow:hidden;">
                <a href="#" id="preview-link" class="lasso-selected" onclick="return false;" style="text-decoration:none; color: inherit;">Exemple de lien sélectionné</a>
            </div>
            <textarea id="custom-css-input" class="gm-popup-input" style="display:none; width:100%; height:80px; text-align:left; font-family:monospace;" placeholder=".lasso-selected {\\n  outline: 2px dotted red !important;\\n}"></textarea>
            <div class="gm-popup-footer">
                <button id="btn-save" class="gm-btn-action" type="button" accesskey="e"><u>E</u>nregistrer</button>
                <button id="btn-cancel" class="gm-btn-action" type="button" accesskey="a"><u>A</u>nnuler</button>
                <button id="btn-reset" class="gm-btn-action" type="button" accesskey="r"><u>R</u>éinitialiser</button>
            </div>
        `;

        overlay.appendChild(popup); document.body.appendChild(overlay);

        // Mappage des éléments DOM de la modale
        const select = document.getElementById('style-select'), textarea = document.getElementById('custom-css-input');
        const mouseCopySelect = document.getElementById('mouse-copy-select'), mouseOpenSelect = document.getElementById('mouse-open-select');
        const copyCtrl = document.getElementById('chk-copy-ctrl'), copyMeta = document.getElementById('chk-copy-meta'), copyAlt = document.getElementById('chk-copy-alt'), copyShift = document.getElementById('chk-copy-shift');
        const openCtrl = document.getElementById('chk-open-ctrl'), openMeta = document.getElementById('chk-open-meta'), openAlt = document.getElementById('chk-open-alt'), openShift = document.getElementById('chk-open-shift'), openForeground = document.getElementById('chk-open-foreground');
        const lblCopyCtrl = document.getElementById('lbl-copy-ctrl'), lblCopyMeta = document.getElementById('lbl-copy-meta'), lblCopyAlt = document.getElementById('lbl-copy-alt'), lblCopyShift = document.getElementById('lbl-copy-shift');
        const lblOpenCtrl = document.getElementById('lbl-open-ctrl'), lblOpenMeta = document.getElementById('lbl-open-meta'), lblOpenAlt = document.getElementById('lbl-open-alt'), lblOpenShift = document.getElementById('lbl-open-shift'), lblOpenForeground = document.getElementById('lbl-open-foreground');
        const txtOpenForeground = document.getElementById('txt-open-foreground');

        // Restauration des états sauvegardés
        copyCtrl.checked = GM_getValue('copyCtrl', true); copyMeta.checked = GM_getValue('copyMeta', false); copyAlt.checked = GM_getValue('copyAlt', false); copyShift.checked = GM_getValue('copyShift', false);
        mouseCopySelect.value = GM_getValue('copyButton', 2);
        openCtrl.checked = GM_getValue('openCtrl', true); openMeta.checked = GM_getValue('openMeta', true); openAlt.checked = GM_getValue('openAlt', false); openShift.checked = GM_getValue('openShift', false);
        mouseOpenSelect.value = GM_getValue('openButton', 2);
        openForeground.checked = GM_getValue('openInForeground', false);

        const pills = [
            {chk: copyCtrl, lbl: lblCopyCtrl}, {chk: copyMeta, lbl: lblCopyMeta}, {chk: copyAlt, lbl: lblCopyAlt}, {chk: copyShift, lbl: lblCopyShift},
            {chk: openCtrl, lbl: lblOpenCtrl}, {chk: openMeta, lbl: lblOpenMeta}, {chk: openAlt, lbl: lblOpenAlt}, {chk: openShift, lbl: lblOpenShift}
        ];

        const syncForegroundState = () => {
            const isFg = openForeground.checked;
            Object.assign(lblOpenForeground.style, isFg ? { background: '#00ff00', color: '#000000' } : { background: '#111', color: '#00ff00' });
            txtOpenForeground.textContent = isFg ? "Avant-plan 👁️" : "Arrière-plan 💤";
        };

        // Empêche la validation de conflits identiques stricts de déclencheurs clavier/souris
        const checkTriggerConflict = () => {
            const bCopy = parseInt(mouseCopySelect.value, 10), bOpen = parseInt(mouseOpenSelect.value, 10);
            const isConflict = (copyCtrl.checked === openCtrl.checked && copyMeta.checked === openMeta.checked && copyAlt.checked === openAlt.checked && copyShift.checked === openShift.checked && bCopy === bOpen);
            document.getElementById('trigger-error').style.display = isConflict ? 'block' : 'none';
            const saveButton = document.getElementById('btn-save');
            saveButton.disabled = isConflict;
            Object.assign(saveButton.style, isConflict ? { opacity: '0.4', cursor: 'not-allowed', pointerEvents: 'none' } : { opacity: '1', cursor: 'pointer', pointerEvents: 'auto' });
        };

        pills.forEach(p => {
            p.lbl.classList.toggle('active', p.chk.checked);
            p.chk.onchange = () => { p.lbl.classList.toggle('active', p.chk.checked); checkTriggerConflict(); };
        });

        syncForegroundState();
        openForeground.onchange = syncForegroundState;
        mouseCopySelect.onchange = mouseOpenSelect.onchange = checkTriggerConflict;

        select.value = initialStyle;
        if(select.value === 'custom') { textarea.style.display = 'block'; textarea.value = initialCustomCSS; }

        const updateLivePreview = () => {
            if (select.value === 'custom') { textarea.style.display = 'block'; StylesManager.apply('custom', textarea.value); }
            else { textarea.style.display = 'none'; StylesManager.apply(select.value); }
        };

        // Intégration de la navigation à la molette sur les sélecteurs déroulants
        const setupWheel = (el) => {
            el.addEventListener('wheel', (e) => {
                e.preventDefault(); e.stopPropagation();
                el.selectedIndex = Math.max(0, Math.min(el.options.length - 1, el.selectedIndex + (e.deltaY > 0 ? 1 : -1)));
                if (el === select) updateLivePreview(); else checkTriggerConflict();
            }, { passive: false });
        };

        [select, mouseCopySelect, mouseOpenSelect].forEach(setupWheel);
        select.onchange = textarea.oninput = updateLivePreview;

        const cancelAction = () => {
            removeModalListeners(); StylesManager.apply(initialStyle, initialCustomCSS); overlay.remove();
        };

        const saveAction = () => {
            const bCopy = parseInt(mouseCopySelect.value, 10), bOpen = parseInt(mouseOpenSelect.value, 10);
            if (copyCtrl.checked === openCtrl.checked && copyMeta.checked === openMeta.checked && copyAlt.checked === openAlt.checked && copyShift.checked === openShift.checked && bCopy === bOpen) return;

            GM_setValue('copyCtrl', copyCtrl.checked); GM_setValue('copyMeta', copyMeta.checked); GM_setValue('copyAlt', copyAlt.checked); GM_setValue('copyShift', copyShift.checked); GM_setValue('copyButton', bCopy);
            GM_setValue('openCtrl', openCtrl.checked); GM_setValue('openMeta', openMeta.checked); GM_setValue('openAlt', openAlt.checked); GM_setValue('openShift', openShift.checked); GM_setValue('openButton', bOpen);
            GM_setValue('openInForeground', openForeground.checked); GM_setValue('selectedStyle', select.value);
            if(select.value === 'custom') GM_setValue('customCSS', textarea.value);

            const saveBtn = document.getElementById('btn-save');
            if (saveBtn) saveBtn.textContent = "Enregistré ! ✅";
            setTimeout(() => { removeModalListeners(); StylesManager.apply(select.value); overlay.remove(); }, 400);
        };

        const resetAction = () => {
            copyCtrl.checked = openCtrl.checked = openMeta.checked = true;
            copyMeta.checked = copyAlt.checked = copyShift.checked = openAlt.checked = openShift.checked = openForeground.checked = false;
            mouseCopySelect.value = mouseOpenSelect.value = 2;
            pills.forEach(p => p.lbl.classList.toggle('active', p.chk.checked));
            syncForegroundState(); checkTriggerConflict();
        };

        document.getElementById('btn-save').onclick = saveAction;
        document.getElementById('btn-cancel').onclick = cancelAction;
        document.getElementById('btn-reset').onclick = resetAction;
        overlay.onclick = (e) => { if(e.target === overlay) cancelAction(); };

        // Intercepteur clavier strict de la modale (Focus trap, Échap, Raccourcis soulignés AccessKey)
        const modalKeyInterceptor = (e) => {
            const inside = e.target.closest('#gm-settings-popup');
            if (e.type === 'keydown') {
                if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); cancelAction(); return; }
                if (e.key === 'Tab') {
                    const vis = Array.from(popup.querySelectorAll('input, select, textarea, button')).filter(el => el.offsetWidth > 0 || el.offsetHeight > 0);
                    if (vis.length) {
                        if (e.shiftKey && e.target === vis[0]) { vis[vis.length - 1].focus(); e.preventDefault(); e.stopImmediatePropagation(); return; }
                        if (!e.shiftKey && e.target === vis[vis.length - 1]) { vis[0].focus(); e.preventDefault(); e.stopImmediatePropagation(); return; }
                    }
                }
                const k = e.key.toLowerCase();
                if ((e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') || (e.altKey && ['e', 'a', 'r'].includes(k))) {
                    e.preventDefault(); e.stopImmediatePropagation();
                    if (k === 'a') cancelAction();
                    else if (k === 'r') resetAction();
                    else if (!document.getElementById('btn-save').disabled) saveAction();
                    return;
                }
            }
            if (!inside) { e.preventDefault(); e.stopImmediatePropagation(); }
        };

        const removeModalListeners = () => ['keydown', 'keyup', 'keypress'].forEach(t => window.removeEventListener(t, modalKeyInterceptor, true));
        ['keydown', 'keyup', 'keypress'].forEach(t => window.addEventListener(t, modalKeyInterceptor, true));

        popup.addEventListener('keydown', (e) => e.stopPropagation());
        popup.addEventListener('keyup', (e) => e.stopPropagation());
        popup.addEventListener('keypress', (e) => e.stopPropagation());

        setTimeout(() => {
            const vis = Array.from(popup.querySelectorAll('input, select, textarea, button')).filter(el => el.offsetWidth > 0 || el.offsetHeight > 0);
            if (vis.length) vis[0].focus();
        }, 50);
    }

    // ==========================================
    // ÉCOUTEURS D'ÉVÉNEMENTS PHYSIQUES (SOURIS / CLAVIER)
    // ==========================================
    document.addEventListener('mouseover', (e) => {
        const link = e.target.closest('a[href]');
        if (link && link.id !== 'preview-link') {
            const h = link.getAttribute('href').trim().toLowerCase();
            if (!h.startsWith('javascript:') && h !== '#' && h !== '') State.hoveredLink = link.href;
        }
    });

    document.addEventListener('mouseout', (e) => { if (e.target.closest('a[href]')) State.hoveredLink = null; });

    document.addEventListener('mousedown', (e) => {
        const action = getActionFromModifiers(e);
        const matchCopy = action === 'copy' && e.button === parseInt(GM_getValue('copyButton', 2), 10);
        const matchOpen = action === 'open' && e.button === parseInt(GM_getValue('openButton', 2), 10);

        if (matchCopy || matchOpen) {
            e.preventDefault(); e.stopPropagation();
            State.isDragging = true;
            State.actionType = matchCopy ? 'copy' : 'open';
            State.liveForeground = GM_getValue('openInForeground', false);
            State.startedWithRightClick = (e.button === 2);
            State.shouldBlockContextMenu = State.startedWithRightClick;
            State.startPageX = e.clientX + window.scrollX; State.startPageY = e.clientY + window.scrollY;
            State.clientMouseX = e.clientX; State.clientMouseY = e.clientY;

            LassoEngine.cacheGeometries();
            Object.assign(UI.lasso.style, { left: State.startPageX + 'px', top: State.startPageY + 'px', width: '0px', height: '0px', display: 'block' });
        } else {
            State.startedWithRightClick = State.shouldBlockContextMenu = false;
        }
    });

    document.addEventListener('mousemove', (e) => {
        if (!State.isDragging) return;
        State.clientMouseX = e.clientX; State.clientMouseY = e.clientY;
        if (!State.rafId) State.rafId = requestAnimationFrame(LassoEngine.render);
    });

    window.addEventListener('scroll', () => {
        if (State.isDragging && !State.rafId) State.rafId = requestAnimationFrame(LassoEngine.render);
    }, { passive: true });

    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && State.isDragging) {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            resetUI(); return;
        }

        // Raccourci instantané Ctrl + C sur survol de lien
        if (e.ctrlKey && e.key.toLowerCase() === 'c' && State.hoveredLink && !State.isDragging && !window.getSelection().toString()) {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            GM_setClipboard(State.hoveredLink); UI.playSuccessChime();
            UI.showToast(`📋 Lien copié :`, State.hoveredLink); return;
        }

        // Ajustements dynamiques en cours de déplacement de lasso
        if (State.isDragging) {
            // INTERCEPTION STRICTE DE LA TOUCHE "A"
            if (e.key.toLowerCase() === 'a' && State.actionType === 'open') {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation(); // Bloque le site internet
                State.liveForeground = !State.liveForeground;
                UI.updateCounter();
                return;
            }
            clearTimeout(State.keyupTimeout);
            const act = getActionFromModifiers(e);
            if (act && act !== State.actionType) { State.actionType = act; UI.updateCounter(); }
        }
    }, true); // <--- "true" active la phase de capture pour passer AVANT le site

    document.addEventListener('keyup', (e) => {
          if (State.isDragging) {
              // BLOCAGE DU KEYUP DE LA TOUCHE "A" POUR LE SITE
              if (e.key.toLowerCase() === 'a' && State.actionType === 'open') {
                  e.preventDefault();
                  e.stopPropagation();
                  e.stopImmediatePropagation(); // Bloque le site internet au relâchement
                  return;
              }

              const act = getActionFromModifiers(e);
              if (act && act !== State.actionType) {
                  clearTimeout(State.keyupTimeout);
                  State.keyupTimeout = setTimeout(() => { if (State.isDragging) { State.actionType = act; UI.updateCounter(); } }, 100);
              } else if (!act) {
                  clearTimeout(State.keyupTimeout);
              }
          }
    }, true); // <--- "true" ici aussi pour intercepter le relâchement avant le site

    document.addEventListener('mouseup', (e) => {
        if (State.isDragging) {
            clearTimeout(State.keyupTimeout);
            if (State.rafId) { cancelAnimationFrame(State.rafId); State.rafId = null; }

            // Sécurité temporelle contre les menus contextuels parasites
            if (State.startedWithRightClick || e.button === 2) {
                e.preventDefault(); e.stopPropagation();
                State.shouldBlockContextMenu = true; State.lastLassoEndTime = Date.now();
                setTimeout(() => { State.shouldBlockContextMenu = false; }, 400);
            }

            const s = State.selectedLinks.length > 1 ? 's' : '';
            if (State.selectedLinks.length) {
                if (State.actionType === 'copy') {
                    GM_setClipboard(State.selectedLinks.join('\n')); UI.playSuccessChime();
                    UI.showToast(`${State.selectedLinks.length} lien${s} copié${s} !`);
                } else if (State.actionType === 'open') {
                    if (State.selectedLinks.length > MAX_TABS_SECURITY && !confirm(`⚠️ Attention : Vous allez ouvrir ${State.selectedLinks.length} onglets en même temps. Continuer ?`)) {
                        resetUI(); return;
                    }
                    State.selectedLinks.forEach(url => GM_openInTab(url, { active: State.liveForeground, insert: true }));
                    UI.playSuccessChime(); UI.showToast(`${State.selectedLinks.length} onglet${s} ouvert${s} !`);
                }
            }
            resetUI();
        }
    });

    document.addEventListener('contextmenu', (e) => {
        if (State.shouldBlockContextMenu || State.isDragging || State.startedWithRightClick || (Date.now() - State.lastLassoEndTime < 400)) {
            e.preventDefault(); e.stopImmediatePropagation(); State.shouldBlockContextMenu = false;
        }
    }, true);

    function resetUI() {
        State.isDragging = false; State.actionType = null; State.cachedLinksGeometry = [];
        clearTimeout(State.keyupTimeout); UI.lasso.style.display = UI.counter.style.display = 'none';
        document.querySelectorAll('.lasso-selected').forEach(a => { if (a.id !== 'preview-link') a.classList.remove('lasso-selected'); });
    }

    // ==========================================
    // INITIALISATION DES COMPOSANTS
    // ==========================================
    StylesManager.init();
    UI.init();
    if (typeof GM_registerMenuCommand !== 'undefined') GM_registerMenuCommand("⚙️ Configurer", createSettingsUI);
})();