Sélectionner, copier ou ouvrir des liens.
// ==UserScript==
// @name MDK Links
// @namespace MDK Scripts
// @version 6.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';
// ==========================================
// CONSTANTES GLOBALES (Lisibilité)
// ==========================================
const MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
const ACTIONS = { COPY: 'copy', OPEN: 'open' };
const MAX_TABS_SECURITY = 25; // Limite de sécurité pour l'ouverture d'onglets
// ==========================================
// 1. ÉTAT GLOBAL DU SCRIPT (STATE)
// ==========================================
const State = {
isDragging: false,
shouldBlockContextMenu: false,
actionType: null,
startPageX: 0, startPageY: 0, // Coordonnées absolues dans le document
clientMouseX: 0, clientMouseY: 0, // Position de la souris à l'écran (viewport)
selectedLinks: [],
cachedLinksGeometry: [],
keyupTimeout: null,
liveForeground: false,
rafId: null
};
// ==========================================
// 2. CATALOGUE DE STYLES
// ==========================================
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', '');
}
};
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: 480px; 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: 200px; 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 10px; 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: 46px; 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; }
/* Styles de la zone des déclencheurs unifiés (Compatible Thèmes) */
.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%; }
`);
// ==========================================
// 3. COMPOSANTS GRAPHIQUES DU LASSO
// ==========================================
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' });
Object.assign(this.toast.style, { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', padding: '10px 20px', backgroundColor: '#28a745', color: '#fff', borderRadius: '5px', zIndex: '1000000', display: 'none', fontFamily: 'sans-serif', fontWeight: 'bold' });
document.body.append(this.lasso, this.counter, this.toast);
},
showToast(msg) {
this.toast.textContent = msg;
this.toast.style.display = 'block';
setTimeout(() => this.toast.style.display = 'none', 2000);
},
updateCounter() {
if (!State.isDragging) return;
if (State.actionType === ACTIONS.COPY) {
this.counter.textContent = `Copier ${State.selectedLinks.length} lien${State.selectedLinks.length > 1 ? 's' : ''}`;
} else if (State.actionType === ACTIONS.OPEN) {
const modeLabel = State.liveForeground ? '👁️ Avant-plan' : '💤 Arrière-plan';
this.counter.textContent = `Ouvrir ${State.selectedLinks.length} lien${State.selectedLinks.length > 1 ? 's' : ''} [${modeLabel}]`;
}
}
};
function getActionFromModifiers(e) {
const matchCopy = (e.ctrlKey === GM_getValue('copyCtrl', true) && e.altKey === GM_getValue('copyAlt', false) && e.shiftKey === GM_getValue('copyShift', false));
const matchOpen = (e.ctrlKey === GM_getValue('openCtrl', true) && e.altKey === GM_getValue('openAlt', false) && e.shiftKey === GM_getValue('openShift', true));
return matchCopy ? ACTIONS.COPY : (matchOpen ? ACTIONS.OPEN : null);
}
// ==========================================
// 4. MOTEUR DU LASSO (COORDONNÉES ABSOLUES PAGE)
// ==========================================
const LassoEngine = {
cacheGeometries() {
const scrollX = window.scrollX;
const scrollY = window.scrollY;
State.cachedLinksGeometry = Array.from(document.querySelectorAll('a[href]'))
.filter(a => {
if (a.id === 'preview-link') return false;
const hrefAttr = a.getAttribute('href').trim().toLowerCase();
if (hrefAttr.startsWith('javascript:') || hrefAttr === '#' || hrefAttr === '') {
return false;
}
return true;
})
.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() {
if (!State.isDragging) return;
const currentPageX = State.clientMouseX + window.scrollX;
const currentPageY = State.clientMouseY + window.scrollY;
const left = Math.min(currentPageX, State.startPageX);
const top = Math.min(currentPageY, State.startPageY);
const w = Math.abs(currentPageX - State.startPageX);
const h = Math.abs(currentPageY - State.startPageY);
Object.assign(UI.lasso.style, { left: left + 'px', top: top + 'px', width: w + 'px', height: h + 'px' });
const uniqueUrls = new Set();
const len = State.cachedLinksGeometry.length;
for (let i = 0; i < len; i++) {
const item = State.cachedLinksGeometry[i];
const isInside = (item.left < left + w && item.right > left && item.top < top + h && item.bottom > top);
if (isInside) {
uniqueUrls.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(uniqueUrls);
if (State.selectedLinks.length === 0) {
UI.counter.style.display = 'none';
} else {
UI.updateCounter();
UI.counter.style.display = 'block';
const counterWidth = UI.counter.offsetWidth || 120;
const counterHeight = UI.counter.offsetHeight || 30;
const gap = 15;
let targetX = State.clientMouseX + gap;
if (targetX + counterWidth > window.innerWidth) {
targetX = State.clientMouseX - gap - counterWidth;
}
let targetY = State.clientMouseY + gap;
if (targetY + counterHeight > window.innerHeight) {
targetY = State.clientMouseY - gap - counterHeight;
}
targetX = Math.max(5, targetX);
targetY = Math.max(5, targetY);
Object.assign(UI.counter.style, { left: targetX + 'px', top: targetY + 'px' });
}
State.rafId = null;
}
};
// ==========================================
// 5. MODALE DE CONFIGURATION
// ==========================================
function createSettingsUI() {
if (document.getElementById('gm-settings-popup')) return;
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" style="font-family: sans-serif;"><input type="checkbox" id="chk-copy-ctrl">Ctrl</label>
<label class="gm-key-pill" id="lbl-copy-alt" style="font-family: sans-serif;"><input type="checkbox" id="chk-copy-alt">Alt</label>
<label class="gm-key-pill" id="lbl-copy-shift" style="font-family: sans-serif;"><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" style="width: 150px !important; padding: 5px 8px;">
<option value="${MOUSE.LEFT}">Clic Gauche</option>
<option value="${MOUSE.MIDDLE}">Clic Milieu</option>
<option value="${MOUSE.RIGHT}">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: 150px !important; box-sizing: border-box; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin: 0; border-radius: 4px; font-family: sans-serif; 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" style="font-family: sans-serif;"><input type="checkbox" id="chk-open-ctrl">Ctrl</label>
<label class="gm-key-pill" id="lbl-open-alt" style="font-family: sans-serif;"><input type="checkbox" id="chk-open-alt">Alt</label>
<label class="gm-key-pill" id="lbl-open-shift" style="font-family: sans-serif;"><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" style="width: 150px !important; padding: 5px 8px;">
<option value="${MOUSE.LEFT}">Clic Gauche</option>
<option value="${MOUSE.MIDDLE}">Clic Milieu</option>
<option value="${MOUSE.RIGHT}">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>Bascule dynamique durant la sélection :</strong><br>
Changer d'action (Copier ⇄ Ouvrir) en appuyant sur le raccourci clavier correspondant.<br>
Si "Ouvrir" est actif, appuyez sur <strong>A</strong> pour basculer le mode d'ouverture (👁️ ⇄ 💤).
</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: 200px !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 mouseCopySelect = document.getElementById('mouse-copy-select');
const mouseOpenSelect = document.getElementById('mouse-open-select');
const textarea = document.getElementById('custom-css-input');
const copyCtrl = document.getElementById('chk-copy-ctrl'), copyAlt = document.getElementById('chk-copy-alt'), copyShift = document.getElementById('chk-copy-shift');
const openCtrl = document.getElementById('chk-open-ctrl'), 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'), lblCopyAlt = document.getElementById('lbl-copy-alt'), lblCopyShift = document.getElementById('lbl-copy-shift');
const lblOpenCtrl = document.getElementById('lbl-open-ctrl'), 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');
// Initialisation avec les valeurs stockées ou les valeurs par défaut
copyCtrl.checked = GM_getValue('copyCtrl', true); copyAlt.checked = GM_getValue('copyAlt', false); copyShift.checked = GM_getValue('copyShift', false);
mouseCopySelect.value = GM_getValue('copyButton', MOUSE.RIGHT); // Copie par défaut : Ctrl + Clic Droit
openCtrl.checked = GM_getValue('openCtrl', true); openAlt.checked = GM_getValue('openAlt', false); openShift.checked = GM_getValue('openShift', true);
mouseOpenSelect.value = GM_getValue('openButton', MOUSE.RIGHT); // Ouvrir par défaut : Ctrl + Shift + Clic Droit
openForeground.checked = GM_getValue('openInForeground', false);
const syncPillState = (chk, lbl) => lbl.classList.toggle('active', chk.checked);
const syncForegroundState = () => {
if (openForeground.checked) {
lblOpenForeground.style.background = '#00ff00'; lblOpenForeground.style.color = '#000000';
txtOpenForeground.textContent = "Avant-plan 👁️";
} else {
lblOpenForeground.style.background = '#111'; lblOpenForeground.style.color = '#00ff00';
txtOpenForeground.textContent = "Arrière-plan 💤";
}
};
syncPillState(copyCtrl, lblCopyCtrl); syncPillState(copyAlt, lblCopyAlt); syncPillState(copyShift, lblCopyShift);
syncPillState(openCtrl, lblOpenCtrl); syncPillState(openAlt, lblOpenAlt); syncPillState(openShift, lblOpenShift);
syncForegroundState();
const checkTriggerConflict = () => {
const bCopy = parseInt(mouseCopySelect.value, 10), bOpen = parseInt(mouseOpenSelect.value, 10);
const isConflict = (copyCtrl.checked === openCtrl.checked && copyAlt.checked === openAlt.checked && copyShift.checked === openShift.checked && bCopy === bOpen);
const errorElement = document.getElementById('trigger-error');
const saveButton = document.getElementById('btn-save');
errorElement.style.display = isConflict ? 'block' : 'none';
saveButton.disabled = isConflict;
Object.assign(saveButton.style, isConflict ? { opacity: '0.4', cursor: 'not-allowed', pointerEvents: 'none' } : { opacity: '1', cursor: 'pointer', pointerEvents: 'auto' });
};
copyCtrl.onchange = () => { syncPillState(copyCtrl, lblCopyCtrl); checkTriggerConflict(); };
copyAlt.onchange = () => { syncPillState(copyAlt, lblCopyAlt); checkTriggerConflict(); };
copyShift.onchange = () => { syncPillState(copyShift, lblCopyShift); checkTriggerConflict(); };
openCtrl.onchange = () => { syncPillState(openCtrl, lblOpenCtrl); checkTriggerConflict(); };
openAlt.onchange = () => { syncPillState(openAlt, lblOpenAlt); checkTriggerConflict(); };
openShift.onchange = () => { syncPillState(openShift, lblOpenShift); checkTriggerConflict(); };
openForeground.onchange = () => syncForegroundState();
mouseCopySelect.onchange = checkTriggerConflict;
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 = (element) => {
element.addEventListener('wheel', (e) => {
e.preventDefault(); e.stopPropagation();
let idx = element.selectedIndex + (e.deltaY > 0 ? 1 : -1);
element.selectedIndex = Math.max(0, Math.min(element.options.length - 1, idx));
if (element === select) updateLivePreview();
else checkTriggerConflict();
}, { passive: false });
};
setupWheel(select); setupWheel(mouseCopySelect); setupWheel(mouseOpenSelect);
select.onchange = updateLivePreview;
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 && copyAlt.checked === openAlt.checked && copyShift.checked === openShift.checked && bCopy === bOpen) return;
GM_setValue('copyCtrl', copyCtrl.checked); GM_setValue('copyAlt', copyAlt.checked); GM_setValue('copyShift', copyShift.checked); GM_setValue('copyButton', bCopy);
GM_setValue('openCtrl', openCtrl.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 = () => {
// Remise à zéro vers les valeurs d'usine
copyCtrl.checked = true; copyAlt.checked = false; copyShift.checked = false;
mouseCopySelect.value = MOUSE.RIGHT;
openCtrl.checked = true; openAlt.checked = false; openShift.checked = true; // Ctrl + Shift + Clic Droit
mouseOpenSelect.value = MOUSE.RIGHT; openForeground.checked = false;
syncPillState(copyCtrl, lblCopyCtrl); syncPillState(copyAlt, lblCopyAlt); syncPillState(copyShift, lblCopyShift);
syncPillState(openCtrl, lblOpenCtrl); syncPillState(openAlt, lblOpenAlt); syncPillState(openShift, lblOpenShift);
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 eventTypes = ['keydown', 'keyup', 'keypress'];
const modalKeyInterceptor = (e) => {
const isInsideModal = 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 focusable = popup.querySelectorAll('input, select, textarea, button');
const visible = Array.from(focusable).filter(el => el.offsetWidth > 0 || el.offsetHeight > 0);
if (visible.length > 0) {
const first = visible[0];
const last = visible[visible.length - 1];
if (e.shiftKey) {
if (e.target === first) { last.focus(); e.preventDefault(); e.stopImmediatePropagation(); return; }
} else {
if (e.target === last) { first.focus(); e.preventDefault(); e.stopImmediatePropagation(); return; }
}
}
}
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
e.preventDefault(); e.stopImmediatePropagation();
const sBtn = document.getElementById('btn-save');
if(!sBtn.disabled) saveAction(); return;
}
if (e.altKey && e.key.toLowerCase() === 'e') {
e.preventDefault(); e.stopImmediatePropagation();
const sBtn = document.getElementById('btn-save');
if(!sBtn.disabled) saveAction(); return;
}
if (e.altKey && e.key.toLowerCase() === 'a') { e.preventDefault(); e.stopImmediatePropagation(); cancelAction(); return; }
if (e.altKey && e.key.toLowerCase() === 'r') { e.preventDefault(); e.stopImmediatePropagation(); resetAction(); return; }
}
if (!isInsideModal) { e.preventDefault(); e.stopImmediatePropagation(); }
};
const removeModalListeners = () => eventTypes.forEach(type => window.removeEventListener(type, modalKeyInterceptor, true));
eventTypes.forEach(type => window.addEventListener(type, modalKeyInterceptor, true));
popup.addEventListener('keydown', (e) => e.stopPropagation());
popup.addEventListener('keyup', (e) => e.stopPropagation());
popup.addEventListener('keypress', (e) => e.stopPropagation());
setTimeout(() => {
const focusable = popup.querySelectorAll('input, select, textarea, button');
const visible = Array.from(focusable).filter(el => el.offsetWidth > 0 || el.offsetHeight > 0);
if (visible.length > 0) visible[0].focus();
}, 50);
}
// ==========================================
// 6. ÉCOUTEURS ET LOGIQUE GESTUELLE
// ==========================================
document.addEventListener('mousedown', (e) => {
const matchCopy = (e.button === parseInt(GM_getValue('copyButton', MOUSE.RIGHT), 10) && e.ctrlKey === GM_getValue('copyCtrl', true) && e.altKey === GM_getValue('copyAlt', false) && e.shiftKey === GM_getValue('copyShift', false));
const matchOpen = (e.button === parseInt(GM_getValue('openButton', MOUSE.RIGHT), 10) && e.ctrlKey === GM_getValue('openCtrl', true) && e.altKey === GM_getValue('openAlt', false) && e.shiftKey === GM_getValue('openShift', true));
if (matchCopy || matchOpen) {
e.preventDefault();
State.isDragging = true;
State.actionType = matchCopy ? ACTIONS.COPY : ACTIONS.OPEN;
State.liveForeground = GM_getValue('openInForeground', false);
State.shouldBlockContextMenu = (e.button === MOUSE.RIGHT);
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.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) return;
if (!State.rafId) {
State.rafId = requestAnimationFrame(LassoEngine.render);
}
}, { passive: true });
document.addEventListener('keydown', (e) => {
if(e.key === 'Escape' && State.isDragging) { resetUI(); return; }
if (State.isDragging) {
if (e.key.toLowerCase() === 'a' && State.actionType === ACTIONS.OPEN) {
e.preventDefault();
State.liveForeground = !State.liveForeground;
UI.updateCounter();
return;
}
clearTimeout(State.keyupTimeout);
const currentAction = getActionFromModifiers(e);
if (currentAction && currentAction !== State.actionType) {
State.actionType = currentAction;
UI.updateCounter();
}
}
});
document.addEventListener('keyup', (e) => {
if (State.isDragging) {
const currentAction = getActionFromModifiers(e);
if (currentAction && currentAction !== State.actionType) {
clearTimeout(State.keyupTimeout);
State.keyupTimeout = setTimeout(() => {
if (State.isDragging) {
State.actionType = currentAction;
UI.updateCounter();
}
}, 100);
} else if (!currentAction) {
clearTimeout(State.keyupTimeout);
}
}
});
document.addEventListener('mouseup', () => {
if(State.isDragging) {
clearTimeout(State.keyupTimeout);
if(State.rafId) { cancelAnimationFrame(State.rafId); State.rafId = null; }
if(State.selectedLinks.length) {
if (State.actionType === ACTIONS.COPY) {
GM_setClipboard(State.selectedLinks.join('\n'));
UI.showToast(`${State.selectedLinks.length} liens copiés !`);
} else if (State.actionType === ACTIONS.OPEN) {
if (State.selectedLinks.length > MAX_TABS_SECURITY) {
if (!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.showToast(`${State.selectedLinks.length} onglets ouverts !`);
}
}
resetUI();
}
});
document.addEventListener('contextmenu', (e) => { if (State.shouldBlockContextMenu) { e.preventDefault(); State.shouldBlockContextMenu = false; } });
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 ---
StylesManager.init();
UI.init();
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand("⚙️ Configurer", createSettingsUI);
}
})();