AntiAds

Небольшое пользовательское расширение для lolz.live, которое скрывает рекламные аватарки и заменяет их на нейтральные.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AntiAds
// @namespace    tm_lzt_ads_replacer
// @description  Небольшое пользовательское расширение для lolz.live, которое скрывает рекламные аватарки и заменяет их на нейтральные.
// @version      1.1
// @license      MIT
// @match        https://lolz.live/*
// @match        https://lzt.market/*
// @run-at       document-end
// ==/UserScript==

(() => {
  "use strict";

  /* ================= CONFIG ================= */

  const CFGK="tm_ads_cfg", DBK="tm_ads_db", SINCEK="tm_ads_since",
        ADSUK="tm_ads_u", ADSHK="tm_ads_h", WLUK="tm_ads_wl_u";

  const DEF={
    api:"https://lolz-antiads.vercel.app",
    key:"dontbeafreak",
    sync:180000,
    tick:700,
    max:250,
    on:1,
    rep:"https://nztcdn.com/avatar/l/1766152242/6060330.webp"
  };

  const RX=/nztcdn\.com\/avatar\/([sml])\/(\d+)\/(\d+)\.[a-z0-9]+/i;

  /* ================= HELPERS ================= */

  const J=(k,d)=>{try{return JSON.parse(localStorage.getItem(k))??d}catch{return d}};
  const W=(k,v)=>{try{localStorage.setItem(k,JSON.stringify(v))}catch{}};

  let cfg=Object.assign({},DEF,J(CFGK,{})); W(CFGK,cfg);
  let db=J(DBK,{});
  let adsU=new Set((J(ADSUK,[])||[]).map(String));
  let adsH=new Set((J(ADSHK,[])||[]).map(String));
  let wlU =new Set((J(WLUK,[]) ||[]).map(String));
  let seen=new WeakSet();

  /* ================= PARSING ================= */

  const parseAny = (s) => {
    s = String(s || "");

    const mUrl = s.match(/url\((['"]?)(.*?)\1\)/i);
    if (mUrl) s = mUrl[2] || s;

    const m = s.match(RX);
    return m ? {
      sz: m[1],
      unix: m[2],
      uid: m[3],
      k: `${m[2]}/${m[3]}`
    } : null;
  };

  const bgUrl = (el) => {
    if (el.style?.backgroundImage && el.style.backgroundImage !== "none")
      return el.style.backgroundImage;

    const raw = el.getAttribute("style") || "";
    const m = raw.match(/background-image\s*:\s*([^;]+)/i);
    if (m) return m[1];

    try {
      const c = getComputedStyle(el).backgroundImage;
      if (c && c !== "none") return c;
    } catch {}

    return "";
  };

  const repURL = (sz) => {
    const p = parseAny(cfg.rep);
    return p
      ? `https://nztcdn.com/avatar/${sz}/${p.unix}/${p.uid}.webp`
      : cfg.rep;
  };

  /* ================= REPLACE ================= */

  const setImg=(img,on,sz)=>{
    const r=repURL(sz);
    if(on){
      if(img.dataset.tmR==="1") return;
      img.dataset.tmR="1";
      img.dataset.tmO=img.src||"";
      img.src=r;
      img.removeAttribute("srcset");
    } else {
      if(img.dataset.tmR!=="1") return;
      img.dataset.tmR="0";
      if(img.dataset.tmO) img.src=img.dataset.tmO;
    }
  };

  const setBg=(el,on,sz,rawBg)=>{
    const r=repURL(sz);
    if(on){
      if(el.dataset.tmRbg==="1") return;
      el.dataset.tmRbg="1";
      el.dataset.tmOBG = rawBg;
      el.style.backgroundImage = `url("${r}")`;
    } else {
      if(el.dataset.tmRbg!=="1") return;
      el.dataset.tmRbg="0";
      if(el.dataset.tmOBG) el.style.backgroundImage = el.dataset.tmOBG;
    }
  };

  /* ================= LOGIC ================= */

  const isAds = (p) => {
    if (wlU.has(String(p.uid))) return false;
    const h = db[p.k];
    return adsU.has(String(p.uid)) || (h && adsH.has(String(h)));
  };

  const handleImg = (img) => {
    const p = parseAny(img.src || img.getAttribute("data-src"));
    if(!p) return false;
    cfg.on && isAds(p) ? setImg(img,1,p.sz) : setImg(img,0,p.sz);
    if(!db[p.k]) enqueue(p.k);
    return true;
  };

  const handleBg = (el) => {
    if(!el.classList?.contains("img")) return false;
    const rawBg = bgUrl(el);
    const p = parseAny(rawBg);
    if(!p) return false;
    cfg.on && isAds(p) ? setBg(el,1,p.sz,rawBg) : setBg(el,0,p.sz,rawBg);
    if(!db[p.k]) enqueue(p.k);
    return true;
  };

  /* ================= RESOLVE ================= */

  const Q=[], QS=new Set(); let busy=0, last=0;
  const enqueue=k=>{ if(document.hidden||db[k]||QS.has(k)) return; QS.add(k); Q.push(k); };

  const pump=async ()=>{
    if(document.hidden||busy||!Q.length) return;
    busy=1;
    const wait=Math.max(0,350-(Date.now()-last));
    if(wait) await new Promise(r=>setTimeout(r,wait));
    const k=Q.shift(); QS.delete(k);

    if(!db[k]){
      const [unix,uid]=k.split("/");
      try{
        const r=await fetch(`${cfg.api}/api/resolve`,{
          method:"POST",
          headers:{ "Content-Type":"application/json","X-API-Key":cfg.key },
          body:JSON.stringify({unix:+unix,uid:+uid})
        });
        if(r.ok){
          const j=await r.json();
          if(j?.key&&j?.hash){ db[j.key]=j.hash; W(DBK,db); }
        }
      }catch{}
    }
    last=Date.now(); busy=0;
  };

  /* ================= SYNC ================= */

  const sync=async ()=>{
    if(document.hidden) return;
    const since=+localStorage.getItem(SINCEK)||0;
    try{
      const r=await fetch(`${cfg.api}/api/known?since=${since}`,{
        headers:{"X-API-Key":cfg.key}
      });
      if(!r.ok) return;
      const j=await r.json();

      let ch=0;
      for(const k in j.results||{}) if(!db[k]){ db[k]=j.results[k]; ch=1; }

      const u0=adsU.size,h0=adsH.size,w0=wlU.size;
      j.ads_users?.forEach(u=>adsU.add(String(u)));
      j.ads_hashes?.forEach(h=>adsH.add(String(h)));
      j.whitelist_users?.forEach(u=>wlU.add(String(u)));

      if(ch) W(DBK,db);
      if(adsU.size!==u0) W(ADSUK,[...adsU]);
      if(adsH.size!==h0) W(ADSHK,[...adsH]);
      if(wlU.size!==w0)  W(WLUK,[...wlU]);

      if(j.last_updated_at) localStorage.setItem(SINCEK,String(j.last_updated_at));
      if(ch||adsU.size!==u0||adsH.size!==h0||wlU.size!==w0) seen=new WeakSet();
    }catch{}
  };

  /* ================= SCAN ================= */

  const tick=()=>{
    if(document.hidden) return;
    const nodes=document.querySelectorAll("img,.img,[style*='nztcdn.com/avatar/']");
    let n=0;
    for(let i=0;i<nodes.length && n<cfg.max;i++){
      const el=nodes[i];
      if(seen.has(el)) continue;
      const ok = el.tagName==="IMG" ? handleImg(el) : handleBg(el);
      if(ok){ seen.add(el); n++; }
    }
    pump();
  };

  /* ================= VIEW FULL AVATAR ================= */

  document.addEventListener("click",e=>{
    const el=e.target.closest(".img, img");
    if(!el) return;

    const p=parseAny(el.src||bgUrl(el));
    if(!p) return;

    e.preventDefault();
    e.stopPropagation();

    const ov=document.createElement("div");
    ov.style.cssText="position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:999999;display:flex;align-items:center;justify-content:center";
    const im=document.createElement("img");
    im.src=`https://nztcdn.com/avatar/l/${p.unix}/${p.uid}.webp`;
    im.style.cssText="max-width:90vw;max-height:90vh;object-fit:contain;border-radius:14px;background:#000";
    ov.appendChild(im);
    ov.onclick=()=>ov.remove();
    document.body.appendChild(ov);
  },true);

  /* ================= TIMERS ================= */

  let tTick=0,tSync=0;
  const restart=()=>{ clearInterval(tTick); clearInterval(tSync); tTick=setInterval(tick,cfg.tick); tSync=setInterval(sync,cfg.sync); };

  document.addEventListener("visibilitychange",()=>{
    if(document.hidden){ clearInterval(tTick); clearInterval(tSync); }
    else { restart(); sync(); }
  });

  restart(); sync(); tick();
})();