// ==UserScript==
// @name JVC Image Blacklist
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Permet de blacklister des images (émojis, stickers, PP). Mettez votre souris sur l'image à blacklister et cliquez sur le ⛔ qui apparaît pour l'utiliser.
// @match https://www.jeuxvideo.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'jvcImageBlacklist';
const PANEL_ID = 'jvc-image-blacklist-panel';
const TOGGLE_ID = 'jvc-image-blacklist-toggle';
const STYLE_ID = 'jvc-image-blacklist-style';
const EXCEPTIONS = ['https://risibank.fr/logo.png'];
const DEFAULT_AVATAR = 'https://image.jeuxvideo.com/avatar-md/default.jpg';
// ---------- util ----------
function basenameOf(url) {
if (!url) return '';
try { const u = new URL(url, location.href); return (u.pathname.split('/').pop() || '').split('?')[0]; }
catch { const p = String(url).split('/'); return (p[p.length - 1] || '').split('?')[0]; }
}
function normalizeName(name) {
if (!name) return '';
let s = decodeURIComponent(String(name)).toLowerCase();
s = s.replace(/(?:thumb|mini(?:s)?|small|med|mask|preview|vignette)[\._-]?/g, '');
s = s.replace(/-\d+x\d+(?:\.\w+)?/g, '');
s = s.replace(/[_\-.]+/g, '-').replace(/^-+|-+$/g, '');
return s;
}
function loadBlacklist() {
try {
const data = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (!data) return [];
if (typeof data[0] === 'string') return data.map((u) => ({ original: u, display: u, basename: basenameOf(u) }));
return data;
} catch { return []; }
}
function saveBlacklist(list) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
updateCSS(list);
updateToggle(list);
}
function addToBlacklist({ original, display, basename }) {
if (EXCEPTIONS.includes(display) || EXCEPTIONS.includes(original)) return;
const list = loadBlacklist();
if (list.some((it) => it.basename === basename)) return;
list.push({ original, display, basename });
saveBlacklist(list);
}
function removeFromBlacklist(bn) { saveBlacklist(loadBlacklist().filter((it) => it.basename !== bn)); }
// ---------- CSS ----------
function createStyle() {
if (!document.getElementById(STYLE_ID)) {
const s = document.createElement('style'); s.id = STYLE_ID; document.head.appendChild(s);
}
// base rule for avatars replaced by attribute (ensures persistence even if src is rewritten)
const baseCSS = `.conteneur-message img[data-jvc-avatar-replaced="1"]{ content: url("${DEFAULT_AVATAR}") !important; }`;
document.getElementById(STYLE_ID).textContent = baseCSS;
}
function updateCSS(list) {
// we keep the base avatar rule in createStyle; here we append selectors to hide non-avatar images
const styleEl = document.getElementById(STYLE_ID);
if (!styleEl) return;
const base = `.conteneur-message img:not([data-blacklist-preview="true"])`;
const selectors = [];
for (const it of list) {
const bn = normalizeName(basenameOf(it.basename || it.display || it.original || ''));
const disp = it.display || '';
const isAvatar = (disp && disp.includes('/avatar')) || (it.basename && it.basename.includes('avatar'));
if (!isAvatar && bn) {
// we match on data-src, src, alt, risibank-original-src (robust)
selectors.push(`${base}[src*="${bn}"]`);
selectors.push(`${base}[data-src*="${bn}"]`);
selectors.push(`${base}[alt*="${bn}"]`);
selectors.push(`${base}[risibank-original-src*="${bn}"]`);
}
}
// keep existing avatar rule + add hide selectors
const avatarRule = `.conteneur-message img[data-jvc-avatar-replaced="1"]{ content: url("${DEFAULT_AVATAR}") !important; }`;
styleEl.textContent = avatarRule + (selectors.length ? '\n' + selectors.join(',') + ' { display:none !important; pointer-events:none !important; }' : '');
}
// ---------- UI ----------
function createPanel() {
if (document.getElementById(PANEL_ID)) return;
const panel = document.createElement('div'); panel.id = PANEL_ID;
Object.assign(panel.style, { position: 'fixed', top: '160px', right: '12px', backgroundColor: '#1e1e1e', color: '#fff',
padding: '10px', borderRadius: '8px', border: '1px solid #333', maxHeight: '380px', overflowY: 'auto', width: '360px',
zIndex: 999999, fontSize: '13px', display: 'none', boxSizing: 'border-box' });
const title = document.createElement('div'); title.textContent = 'Images blacklistées'; title.style.fontWeight = '700'; title.style.marginBottom = '8px';
const content = document.createElement('div'); content.id = PANEL_ID + '-content';
Object.assign(content.style, { display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: '10px', boxSizing: 'border-box' });
panel.appendChild(title); panel.appendChild(content); document.body.appendChild(panel);
}
function createToggle() {
if (document.getElementById(TOGGLE_ID)) return;
const btn = document.createElement('button'); btn.id = TOGGLE_ID; btn.textContent = '🗑 Blacklist';
Object.assign(btn.style, { position: 'fixed', bottom: '18px', right: '18px', zIndex: 999999, padding: '8px 12px', backgroundColor: '#2d2d2d',
color: '#eee', borderRadius: '8px', border: '1px solid #444', cursor: 'pointer', display: 'none' });
btn.onclick = ()=>{ const p = document.getElementById(PANEL_ID); if(!p) return; p.style.display = (p.style.display==='block') ? 'none' : 'block'; renderPanel(); };
document.body.appendChild(btn);
}
function updateToggle(list) { const t = document.getElementById(TOGGLE_ID); if (!t) return; t.style.display = list && list.length ? 'block' : 'none'; }
function renderPanel() {
const content = document.getElementById(PANEL_ID + '-content'); if(!content) return;
const list = loadBlacklist(); content.innerHTML = ''; if(!list.length){ content.textContent = 'Aucune image blacklistée.'; return; }
for(const it of list){
const box=document.createElement('div'); Object.assign(box.style,{display:'flex',flexDirection:'column',alignItems:'center',boxSizing:'border-box'});
const img=document.createElement('img'); img.dataset.blacklistPreview='true';
img.src = it.display || it.original || '';
Object.assign(img.style,{ maxWidth:'100px', maxHeight:'90px', objectFit:'contain', border:'1px solid #444', background:'#111', padding:'4px', boxSizing:'border-box', pointerEvents:'none' });
const rm=document.createElement('button'); rm.textContent='Retirer'; Object.assign(rm.style,{ marginTop:'6px', backgroundColor:'#8a2b2b', color:'#fff', border:'none', borderRadius:'4px', padding:'4px 8px', cursor:'pointer', fontSize:'12px' });
rm.onclick = ()=>{ removeFromBlacklist(it.basename); renderPanel(); };
box.appendChild(img); box.appendChild(rm); content.appendChild(box);
}
}
// ---------- core processing ----------
function getDisp(img){ return img.dataset.src||img.currentSrc||img.getAttribute('data-src')||img.src||''; }
function getOrig(img){ return img.dataset.src||img.getAttribute('risibank-original-src')||img.getAttribute('data-src')||img.alt||img.src||''; }
function isAvatarCandidate(display, original, img) {
return ((display && display.includes('/avatar')) || (original && original.includes('/avatar')) || img.classList.contains('avatar') || img.classList.contains('user-avatar-msg'));
}
function wrapImage(img) {
if (img.closest('.smileys, .smileys__modal, .smileys__table')) return img.parentElement;
if (img.parentElement && img.parentElement.dataset && img.parentElement.dataset.jvcWrapper === '1') return img.parentElement;
const wrapper = document.createElement('span'); wrapper.style.display='inline-block'; wrapper.style.position='relative'; wrapper.dataset.jvcWrapper='1';
img.parentNode.insertBefore(wrapper, img); wrapper.appendChild(img); return wrapper;
}
// attach button safely (idempotent)
function attachButtonTo(img) {
if (img.dataset.jvcBtnAttached === '1') return;
const wrapper = wrapImage(img);
const btn = document.createElement('button'); btn.textContent='⛔';
Object.assign(btn.style, { position:'absolute', top:'4px', right:'4px', backgroundColor:'rgba(0,0,0,0.55)', color:'#fff', border:'none', borderRadius:'4px', fontSize:'12px', padding:'2px 6px', cursor:'pointer', display:'none', zIndex:9999, lineHeight:'1'});
wrapper.addEventListener('mouseenter',()=>{ btn.style.display='block'; }); wrapper.addEventListener('mouseleave',()=>{ btn.style.display='none'; });
btn.addEventListener('click', (e)=>{
e.preventDefault(); e.stopImmediatePropagation(); e.stopPropagation();
const link = img.closest('a'); if(link){ try{ link.removeAttribute('href'); link.onclick = (ev)=>{ ev.preventDefault(); ev.stopImmediatePropagation(); return false; }; }catch(e){} }
const display = getDisp(img), original = getOrig(img);
const bn = normalizeName(basenameOf(original)) || normalizeName(basenameOf(display)) || '';
addToBlacklist({ original, display, basename: bn });
// immediate visual apply:
if (isAvatarCandidate(display, original, img)) {
try{ img.dataset.jvcAvatarReplaced = '1'; img.src = DEFAULT_AVATAR; }catch(e){}
img.style.pointerEvents = 'none';
} else {
img.style.display = 'none'; img.style.pointerEvents = 'none';
}
renderPanel();
return false;
}, { capture: true });
wrapper.appendChild(btn);
img.dataset.jvcBtnAttached = '1';
}
// process single image: check blacklist and apply replace/hide or attach button
function processImage(img, list) {
if (!img || img.closest('.smileys, .smileys__modal, .smileys__table')) return;
// do NOT permanently block reprocessing; only avoid rapid double-processing via temp flag
if (img.dataset.jvcProcessing === '1') return;
img.dataset.jvcProcessing = '1';
setTimeout(()=>{ try{ delete img.dataset.jvcProcessing; }catch(e){} }, 300);
const display = getDisp(img) || '';
const original = getOrig(img) || '';
if (EXCEPTIONS.includes(display) || EXCEPTIONS.includes(original)) { return; }
// robust check using normalized names
const banned = (function(){
const bnDisp = normalizeName(basenameOf(display));
const bnOrig = normalizeName(basenameOf(original));
const set = new Set(list.map(it => normalizeName(it.basename||'')));
if (set.has(bnDisp) || set.has(bnOrig)) return true;
for (const it of list) {
const stored = normalizeName(it.basename || it.display || it.original || '');
if (!stored) continue;
if (stored.includes(bnDisp) || stored.includes(bnOrig) || bnDisp.includes(stored) || bnOrig.includes(stored)) return true;
}
return false;
})();
if (banned) {
// apply replacement/hide **and mark** so CSS rule can persist display for avatars
const isAvatar = isAvatarCandidate(display, original, img);
if (isAvatar) {
try { img.dataset.jvcAvatarReplaced = '1'; img.src = DEFAULT_AVATAR; } catch(e){}
img.style.pointerEvents = 'none';
// remove any button if present
try { if (img.parentElement && img.parentElement.querySelector('button')) img.parentElement.querySelectorAll('button').forEach(b=>b.remove()); } catch(e){}
} else {
img.style.display = 'none'; img.style.pointerEvents = 'none';
try { if (img.parentElement && img.parentElement.querySelector('button')) img.parentElement.querySelectorAll('button').forEach(b=>b.remove()); } catch(e){}
}
return;
}
// Not banned -> attach button (idempotent)
attachButtonTo(img);
}
// scan all images in posts
function enforceAllOnce() {
const list = loadBlacklist();
document.querySelectorAll('.conteneur-message img').forEach(img => processImage(img, list));
}
// start observer with guards
function startObserver() {
const obs = new MutationObserver((mutations) => {
const list = loadBlacklist();
for (const m of mutations) {
if (m.type === 'childList') {
for (const n of m.addedNodes) {
if (n.nodeType !== 1) continue;
if (n.tagName === 'IMG' && n.closest('.conteneur-message')) processImage(n, list);
if (n.querySelectorAll) n.querySelectorAll('.conteneur-message img').forEach(i => processImage(i, list));
}
} else if (m.type === 'attributes') {
const t = m.target;
if (t && t.tagName === 'IMG' && t.closest('.conteneur-message')) processImage(t, list);
}
}
});
obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src','data-src','alt'] });
}
// repeated enforcement for first N seconds to catch late lazy loaders / third-party scripts
function startPeriodicEnforcement() {
let runs = 0;
const maxRuns = 12; // e.g., ~12 * 500ms = 6s
const id = setInterval(()=>{
try { enforceAllOnce(); } catch(e){}
runs++;
if (runs >= maxRuns) clearInterval(id);
}, 500);
}
// ---------- bootstrap ----------
function init() {
createStyle(); createPanel(); createToggle();
updateCSS(loadBlacklist()); updateToggle(loadBlacklist());
enforceAllOnce();
startObserver();
startPeriodicEnforcement();
}
init();
})();