JVC Image Blacklist

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.

// ==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();

})();