Sélectionner, copier ou ouvrir des liens. Option Ctrl+C sur survol instantané dès le chargement.
// ==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 });
}
})();