Global Text Remover (FR/EN)

Removes specified words/phrases from all sites (case-insensitive, persistent globally, bilingual UI).

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Global Text Remover (FR/EN)
// @name:fr      Masqueur global de texte (FR/EN)
// @namespace    https://example.com
// @version      3.5
// @description  Removes specified words/phrases from all sites (case-insensitive, persistent globally, bilingual UI).
// @description:fr  Script ChatGPT qui enlève des mots/phrases de tous les sites (ne respecte pas la casse, persiste).
// @match        *://*/*
// @grant        GM_getValue
// @license MIT
// @grant        GM_setValue
// ==/UserScript==

(function() {
  "use strict";

  /** 🌐 Language auto-detect **/
  const lang = navigator.language.startsWith("fr") ? "fr" : "en";

  const T = {
    addPlaceholder: lang === "fr" ? "Mot ou phrase à supprimer" : "Word or phrase to remove",
    addButton: lang === "fr" ? "Ajouter" : "Add",
    panelTitle: lang === "fr" ? "Liste de mots masqués" : "Hidden words list",
    deleteTip: lang === "fr" ? "Supprimer ce mot" : "Delete this word",
    hideButton: lang === "fr" ? "Cacher le bouton" : "Hide button"
  };

  const STORAGE_KEY = "masqueur_global_list";
  const HIDE_KEY = "masqueur_toggle_hidden";

  let filtres = GM_getValue(STORAGE_KEY, []);
  let toggleHidden = GM_getValue(HIDE_KEY, false);

  /** Utils **/
  const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

  const isIgnored = el => {
    if (!el) return true;
    if (el.closest("#masqueur-panel") || el.closest("#masqueur-toggle")) return true;
    const tag = el.tagName ? el.tagName.toLowerCase() : "";
    return ["input","textarea","select","script","style","noscript"].includes(tag);
  };

  /** Masquage **/
  function maskNodeText(node) {
    if (node.nodeType !== Node.TEXT_NODE) return;
    if (isIgnored(node.parentElement)) return;

    const txt = node.nodeValue;
    if (!txt || !txt.trim()) return;

    let newTxt = txt;

    for (const f of filtres) {
      if (!f) continue;
      const regex = new RegExp(escapeRegex(f), "gi");
      newTxt = newTxt.replace(regex, "");
    }

    if (newTxt !== txt) node.nodeValue = newTxt;
  }

  function maskAll(root = document.body) {

    if (!filtres.length || !root) return;

    const walker = document.createTreeWalker(
      root,
      NodeFilter.SHOW_TEXT,
      null,
      false
    );

    let n;

    while ((n = walker.nextNode())) maskNodeText(n);
  }

  /** Observer **/
  const observer = new MutationObserver(mutations => {

    for (const m of mutations) {

      for (const n of m.addedNodes) {

        if (n.nodeType === Node.TEXT_NODE)
          maskNodeText(n);

        else if (n.nodeType === Node.ELEMENT_NODE && !isIgnored(n))
          maskAll(n);
      }
    }
  });

  /** Interface **/

  // --- Bouton rond ---
  const toggle = document.createElement("div");

  toggle.id = "masqueur-toggle";
  toggle.textContent = "🕵️";

  Object.assign(toggle.style,{
    position:"fixed",
    bottom:"14px",
    left:"14px",
    width:"32px",
    height:"32px",
    lineHeight:"28px",
    textAlign:"center",
    background:"rgba(0,0,0,0.7)",
    color:"white",
    borderRadius:"50%",
    cursor:"pointer",
    zIndex:"999999999",
    fontSize:"18px",
    boxShadow:"0 2px 6px rgba(0,0,0,0.4)",
    transition:"background 0.2s ease",
    userSelect:"none"
  });

  toggle.onmouseenter = () => toggle.style.background="rgba(0,0,0,0.9)";
  toggle.onmouseleave = () => toggle.style.background="rgba(0,0,0,0.7)";

  // --- Panneau ---
  const panel = document.createElement("div");

  panel.id="masqueur-panel";

  Object.assign(panel.style,{
    position:"fixed",
    bottom:"60px",
    left:"10px",
    background:"rgba(0,0,0,0.85)",
    color:"#fff",
    padding:"10px",
    borderRadius:"10px",
    fontFamily:"sans-serif",
    zIndex:"999999998",
    width:"260px",
    boxShadow:"0 2px 6px rgba(0,0,0,0.35)",
    display:"none"
  });

  const title=document.createElement("div");

  title.textContent=T.panelTitle;

  Object.assign(title.style,{
    fontWeight:"bold",
    fontSize:"13px",
    marginBottom:"6px",
    textAlign:"center"
  });

  const input=document.createElement("input");

  Object.assign(input,{
    type:"text",
    placeholder:T.addPlaceholder
  });

  Object.assign(input.style,{
    width:"100%",
    padding:"6px",
    borderRadius:"4px",
    border:"none",
    marginBottom:"6px",
    fontSize:"12px",
    boxSizing:"border-box"
  });

  const addBtn=document.createElement("button");

  addBtn.textContent=T.addButton;

  Object.assign(addBtn.style,{
    width:"100%",
    padding:"6px",
    background:"#2e8b57",
    color:"#fff",
    border:"none",
    borderRadius:"4px",
    cursor:"pointer",
    fontSize:"12px",
    marginBottom:"6px"
  });

  const hideBtn=document.createElement("button");

  hideBtn.textContent=T.hideButton;

  Object.assign(hideBtn.style,{
    width:"100%",
    padding:"6px",
    background:"#444",
    color:"#fff",
    border:"none",
    borderRadius:"4px",
    cursor:"pointer",
    fontSize:"12px",
    marginBottom:"6px"
  });

  const list=document.createElement("ul");

  Object.assign(list.style,{
    listStyle:"none",
    padding:0,
    margin:0,
    maxHeight:"150px",
    overflowY:"auto",
    fontSize:"12px"
  });

  /** Flèche de réapparition **/

  const revealZone=document.createElement("div");

  Object.assign(revealZone.style,{
    position:"fixed",
    bottom:"0",
    left:"0",
    width:"40px",
    height:"40px",
    zIndex:"999999997"
  });

  const revealArrow=document.createElement("div");

  revealArrow.textContent="➜";

  Object.assign(revealArrow.style,{
    position:"fixed",
    bottom:"14px",
    left:"10px",
    fontSize:"18px",
    color:"white",
    background:"rgba(0,0,0,0.7)",
    borderRadius:"6px",
    padding:"2px 6px",
    cursor:"pointer",
    display:"none",
    zIndex:"999999998"
  });

  revealZone.onmouseenter=()=>{
    if(toggleHidden) revealArrow.style.display="block";
  };

  revealZone.onmouseleave=()=>{
    revealArrow.style.display="none";
  };

  revealArrow.onclick=()=>{
    toggleHidden=false;
    GM_setValue(HIDE_KEY,false);
    toggle.style.display="block";
    revealArrow.style.display="none";
  };

  /** Liste **/

  function renderList(){

    list.innerHTML="";

    filtres.forEach((f,i)=>{

      const li=document.createElement("li");

      Object.assign(li.style,{
        display:"flex",
        justifyContent:"space-between",
        alignItems:"center",
        padding:"3px 0",
        borderBottom:"1px solid rgba(255,255,255,0.1)"
      });

      const span=document.createElement("span");
      span.textContent=f;

      const del=document.createElement("button");

      del.textContent="✖";
      del.title=T.deleteTip;

      Object.assign(del.style,{
        background:"none",
        border:"none",
        color:"#ff6666",
        cursor:"pointer",
        fontSize:"12px"
      });

      del.onclick=()=>{

        filtres.splice(i,1);

        GM_setValue(STORAGE_KEY,filtres);

        maskAll();

        renderList();
      };

      li.appendChild(span);
      li.appendChild(del);

      list.appendChild(li);
    });
  }

  addBtn.onclick=()=>{

    const val=input.value.trim();

    if(!val) return;

    if(!filtres.includes(val)){

      filtres.push(val);

      GM_setValue(STORAGE_KEY,filtres);

      maskAll();

      renderList();
    }

    input.value="";
  };

  hideBtn.onclick=()=>{

    toggleHidden=true;

    GM_setValue(HIDE_KEY,true);

    toggle.style.display="none";

    panel.style.display="none";
  };

  toggle.onclick=()=>{
    panel.style.display=panel.style.display==="none"?"block":"none";
  };

  panel.appendChild(title);
  panel.appendChild(input);
  panel.appendChild(addBtn);
  panel.appendChild(hideBtn);
  panel.appendChild(list);

  document.body.appendChild(panel);
  document.body.appendChild(toggle);
  document.body.appendChild(revealZone);
  document.body.appendChild(revealArrow);

  /** Initialisation **/

  function init(){

    renderList();

    maskAll();

    observer.observe(document.body,{
      childList:true,
      subtree:true
    });

    if(toggleHidden){
      toggle.style.display="none";
    }
  }

  if(document.readyState==="loading"){
    document.addEventListener("DOMContentLoaded",init);
  } else {
    init();
  }

})();