MDK Links

Sélectionner, copier ou ouvrir des liens. Option Ctrl+C sur survol instantané dès le chargement.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         MDK Links
// @namespace    MDK Scripts
// @version      7.3
// @description  Sélectionner, copier ou ouvrir des liens. Option Ctrl+C sur survol instantané dès le chargement.
// @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
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    /* ==========================================================================
       SECTION 1 : CONFIGURATION ET ÉTAT GLOBAL
       ========================================================================== */

    const MAX_TABS_SECURITY = 25;

    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,
        lastScrollX: 0,
        lastScrollY: 0,
        cacheDebounceTimer: null,
    };

    /* ==========================================================================
       SECTION 2 : GESTION DES THÈMES ET INJECTION DES STYLES CSS
       ========================================================================== */

    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) {
            if (this.element) {
                this.element.textContent = this.catalog[key]?.css || customCSS || GM_getValue('customCSS', '');
            }
        },
    };

    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; }
    `);

    /* ==========================================
       SECTION 3 : COMPOSANTS AUDIO ET NOTIFICATIONS (UI)
       ========================================== */

    let _audioCtx = null;

    function getAudioContext() {
        if (_audioCtx && _audioCtx.state !== 'closed') return _audioCtx;
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (!AudioCtx) return null;
        _audioCtx = new AudioCtx();
        return _audioCtx;
    }

    const UI = {
        lasso: null,
        counter: null,
        toast: null,
        init() {
            this.lasso = document.createElement('div');
            this.counter = document.createElement('div');
            this.toast = document.createElement('div');

            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 = '';

            const titleEl = document.createElement('div');
            titleEl.className = 'gm-toast-title';
            titleEl.textContent = title;
            this.toast.appendChild(titleEl);

            if (content) {
                const contentEl = document.createElement('div');
                contentEl.className = 'gm-toast-content';
                contentEl.textContent = content;
                this.toast.appendChild(contentEl);
            }

            this.toast.classList.add('show');
            State.toastTimeout = setTimeout(() => this.toast.classList.remove('show'), 2800);
        },

        updateCounter() {
            if (!State.isDragging) return;
            const n = State.selectedLinks.length;
            const s = n > 1 ? 's' : '';
            if (State.actionType === 'copy') {
                this.counter.textContent = `Copier ${n} lien${s}`;
            } else if (State.actionType === 'open') {
                this.counter.textContent = `Ouvrir ${n} lien${s} [${State.liveForeground ? '👁️ Avant-plan' : '💤 Arrière-plan'}]`;
            }
        },

        playSuccessChime() {
            try {
                const ctx = getAudioContext();
                if (!ctx) return;
                if (ctx.state === 'suspended') ctx.resume();
                const playNote = (freq, start, dur) => {
                    const osc = ctx.createOscillator();
                    const 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 (_) {}
        },
    };

    /* ==========================================
       SECTION 4 : VERIFICATION DES RACCOURCIS CLAVIER
       ========================================== */

    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);
    }

    /* ==========================================
       SECTION 5 : MOTEUR GEOMETRIQUE DU LASSO
       ========================================== */

    const LassoEngine = {
        cacheGeometries() {
            const scrollX = window.scrollX, scrollY = window.scrollY;
            const currentlySelected = new Set(
                State.cachedLinksGeometry.filter(item => item.isSelected).map(item => item.el)
            );

            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: currentlySelected.has(a),
                    };
                });
        },

        scheduleCacheRefresh() {
            clearTimeout(State.cacheDebounceTimer);
            State.cacheDebounceTimer = setTimeout(() => LassoEngine.cacheGeometries(), 80);
        },

        render() {
            if (!State.isDragging) { State.rafId = null; return; }

            const threshold = 50, scrollSpeed = 12;
            if      (State.clientMouseY > window.innerHeight - threshold) window.scrollBy(0, scrollSpeed);
            else if (State.clientMouseY < threshold && window.scrollY > 0) window.scrollBy(0, -scrollSpeed);
            if      (State.clientMouseX > window.innerWidth - threshold)  window.scrollBy(scrollSpeed, 0);
            else if (State.clientMouseX < threshold && window.scrollX > 0) window.scrollBy(-scrollSpeed, 0);

            if (window.scrollX !== State.lastScrollX || window.scrollY !== State.lastScrollY) {
                State.lastScrollX = window.scrollX;
                State.lastScrollY = window.scrollY;
                LassoEngine.scheduleCacheRefresh();
            }

            const cx = State.clientMouseX + window.scrollX;
            const 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 = requestAnimationFrame(LassoEngine.render);
        },
    };

    /* ==========================================
       SECTION 6 : POPUP GRAPHISQUE DE CONFIGURATION
       ========================================== */

    function createSettingsUI() {
        if (document.getElementById('gm-settings-popup')) return;
        ensureUIInitialized();

        const initialStyle = GM_getValue('selectedStyle', 'starwars');
        const 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);

        const select         = document.getElementById('style-select');
        const textarea       = document.getElementById('custom-css-input');
        const mouseCopySelect = document.getElementById('mouse-copy-select');
        const mouseOpenSelect = document.getElementById('mouse-open-select');

        const copyCtrl  = document.getElementById('chk-copy-ctrl');
        const copyMeta  = document.getElementById('chk-copy-meta');
        const copyAlt   = document.getElementById('chk-copy-alt');
        const copyShift = document.getElementById('chk-copy-shift');
        const openCtrl  = document.getElementById('chk-open-ctrl');
        const openMeta  = document.getElementById('chk-open-meta');
        const openAlt   = document.getElementById('chk-open-alt');
        const openShift = document.getElementById('chk-open-shift');
        const openForeground = document.getElementById('chk-open-foreground');

        const lblCopyCtrl  = document.getElementById('lbl-copy-ctrl');
        const lblCopyMeta  = document.getElementById('lbl-copy-meta');
        const lblCopyAlt   = document.getElementById('lbl-copy-alt');
        const lblCopyShift = document.getElementById('lbl-copy-shift');
        const lblOpenCtrl  = document.getElementById('lbl-open-ctrl');
        const lblOpenMeta  = document.getElementById('lbl-open-meta');
        const lblOpenAlt   = document.getElementById('lbl-open-alt');
        const lblOpenShift = document.getElementById('lbl-open-shift');
        const lblOpenForeground = document.getElementById('lbl-open-foreground');
        const txtOpenForeground = document.getElementById('txt-open-foreground');

        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 💤';
        };

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

        const checkTriggerConflict = () => {
            const conflict = hasConflict();
            document.getElementById('trigger-error').style.display = conflict ? 'block' : 'none';
            const saveButton = document.getElementById('btn-save');
            saveButton.disabled = conflict;
            Object.assign(saveButton.style, conflict
                ? { 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); }
        };

        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 = () => {
            if (hasConflict()) return;

            GM_setValue('copyCtrl',  copyCtrl.checked);
            GM_setValue('copyMeta',  copyMeta.checked);
            GM_setValue('copyAlt',   copyAlt.checked);
            GM_setValue('copyShift', copyShift.checked);
            GM_setValue('copyButton', parseInt(mouseCopySelect.value, 10));

            GM_setValue('openCtrl',  openCtrl.checked);
            GM_setValue('openMeta',  openMeta.checked);
            GM_setValue('openAlt',   openAlt.checked);
            GM_setValue('openShift', openShift.checked);
            GM_setValue('openButton', parseInt(mouseOpenSelect.value, 10));

            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  = true;
            openCtrl.checked  = true;
            openMeta.checked  = true;
            copyMeta.checked  = false;
            copyAlt.checked   = false;
            copyShift.checked = false;
            openAlt.checked   = false;
            openShift.checked = false;
            openForeground.checked = false;
            mouseCopySelect.value  = 2;
            mouseOpenSelect.value  = 2;
            pills.forEach(p => p.lbl.classList.toggle('active', p.chk.checked));
            select.value = 'starwars';
            textarea.style.display = 'none';
            textarea.value = '';
            StylesManager.apply('starwars');
            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(); };

        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);
    }

    /* ==========================================
       SECTION 7 : ECOUTEURS D'EVENEMENTS PHYSIQUES (DOM)
       ========================================== */

    document.addEventListener('mouseover', (e) => {
        ensureUIInitialized();
        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;
            }
        }
    }, true);

    document.addEventListener('mouseout', (e) => {
        const link = e.target.closest('a[href]');
        if (!link) return;
        const to = e.relatedTarget;
        if (to && link.contains(to)) return;
        State.hoveredLink = null;
    }, true);

    document.addEventListener('mousedown', (e) => {
        ensureUIInitialized();
        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;
            State.lastScrollX = window.scrollX;
            State.lastScrollY = window.scrollY;

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

            if (State.rafId) cancelAnimationFrame(State.rafId);
            State.rafId = requestAnimationFrame(LassoEngine.render);
        } else {
            State.startedWithRightClick = State.shouldBlockContextMenu = false;
        }
    });

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

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

        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;
        }

        if (State.isDragging) {
            if (e.key.toLowerCase() === 'a' && State.actionType === 'open') {
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                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);

    document.addEventListener('keyup', (e) => {
        if (State.isDragging) {
            if (e.key.toLowerCase() === 'a' && State.actionType === 'open') {
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                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);

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

            if (State.startedWithRightClick || e.button === 2) {
                e.preventDefault(); e.stopPropagation();
                State.shouldBlockContextMenu = true;
                State.lastLassoEndTime = Date.now();
                setTimeout(() => { State.shouldBlockContextMenu = false; }, 400);
            }

            const n = State.selectedLinks.length;
            const s = n > 1 ? 's' : '';
            if (n) {
                if (State.actionType === 'copy') {
                    GM_setClipboard(State.selectedLinks.join('\n'));
                    UI.playSuccessChime();
                    UI.showToast(`${n} lien${s} copié${s} !`);
                } else if (State.actionType === 'open') {
                    if (n > MAX_TABS_SECURITY && !confirm(`⚠️ Attention : Vous allez ouvrir ${n} onglets en même temps. Continuer ?`)) {
                        resetUI(); return;
                    }
                    State.selectedLinks.forEach(url => GM_openInTab(url, { active: State.liveForeground, insert: true }));
                    UI.playSuccessChime();
                    UI.showToast(`${n} 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;
        clearTimeout(State.keyupTimeout);
        clearTimeout(State.cacheDebounceTimer);
        if (State.rafId) { cancelAnimationFrame(State.rafId); State.rafId = null; }
        UI.lasso.style.display = UI.counter.style.display = 'none';
        State.cachedLinksGeometry.forEach(item => {
            if (item.isSelected) { item.el.classList.remove('lasso-selected'); item.isSelected = false; }
        });
        State.cachedLinksGeometry = [];
    }

    /* ==========================================
       SECTION 8 : INITIALISATION DE L'INTERFACE (LAZY INITIALIZATION)
       ========================================== */

    let isUIInitialized = false;

    function ensureUIInitialized() {
        if (isUIInitialized) return true;
        if (!document.head || !document.body) return false;

        StylesManager.init();
        UI.init();
        if (typeof GM_registerMenuCommand !== 'undefined') GM_registerMenuCommand('⚙️ Configurer', createSettingsUI);
        isUIInitialized = true;
        return true;
    }

    if (!ensureUIInitialized()) {
        const domObserver = new MutationObserver(() => {
            if (ensureUIInitialized()) domObserver.disconnect();
        });
        domObserver.observe(document.documentElement, { childList: true, subtree: true });
    }
})();