Sélectionner, copier ou ouvrir des liens.
// ==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);
})();