Advanced AdBlocker Panel

Draggable panel, dual counters (page/total), pause, safe-header rules, regex filters (ad-*, -ad*, ad_*, *_ad), Ads/Adverts, and exact 'ad' match (non-regex). Panel only in top frame; blocking in all frames. Trusted Types-safe DOM ops.

// ==UserScript==
// @name         Advanced AdBlocker Panel
// @namespace    baba-scripts
// @version      6.15
// @description  Draggable panel, dual counters (page/total), pause, safe-header rules, regex filters (ad-*, -ad*, ad_*, *_ad), Ads/Adverts, and exact 'ad' match (non-regex). Panel only in top frame; blocking in all frames. Trusted Types-safe DOM ops.
// @author       Volkan SALiH
// @license      MIT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';

  const IS_TOP = (window.top === window.self);

  // === Storage Keys ===
  const KEY_TOTAL   = "abp_total";
  const KEY_PAUSE   = "abp_paused";
  const KEY_OPEN    = "abp_panel_open";

  // Cross-frame shared state
  let totalAds  = Number(GM_getValue(KEY_TOTAL, 0)) || 0;
  let isPaused  = !!GM_getValue(KEY_PAUSE, false);
  let isOpen    = !!GM_getValue(KEY_OPEN, true);

  // Page-local counter
  let pageAds   = 0;

  // === Styles ===
  GM_addStyle(`
    #abp-panel {
      position: fixed; top: 12px; right: 12px; width: 240px;
      background:#1a1a1a; color:#fff; font-family:system-ui, Arial, sans-serif; font-size:13px;
      border-radius: 10px; box-shadow:0 6px 18px rgba(0,0,0,.45);
      z-index: 2147483647; user-select: none; overflow: hidden;
    }
    #abp-header {
      padding:10px; background:#2a2a2a; display:flex; align-items:center; gap:8px;
      font-weight:700; cursor: move;
    }
    #abp-header .shield { font-size: 16px }
    #abp-title { flex: 1; cursor: default }
    #abp-body {
      transition: max-height .35s ease, opacity .35s ease;
      max-height: 420px; opacity: 1; overflow: hidden; padding: 8px 10px;
    }
    #abp-body.collapsed { max-height: 0; opacity: 0; padding: 0 10px }
    .abp-row { padding: 6px 0; border-top: 1px solid #333; display:flex; justify-content:space-between; align-items:center }
    .abp-row:first-of-type { border-top: 0 }
    .abp-btn { cursor:pointer; color:#00b7ff }
    .abp-btn:hover { text-decoration: underline }
  `);

  // === Panel (only in top frame) ===
  let panel, header, body, lblPage, lblTotal, btnPause;
  let dragActive = false, dragMoved = false, dragDX = 0, dragDY = 0;

  if (IS_TOP) {
    panel = document.createElement('div');
    panel.id = 'abp-panel';

    header = document.createElement('div');
    header.id = 'abp-header';

    const icon = document.createElement('span');
    icon.className = 'shield';
    icon.textContent = '🛡️';

    const title = document.createElement('div');
    title.id = 'abp-title';
    title.textContent = 'AdBlock Panel';

    body = document.createElement('div');
    body.id = 'abp-body';

    const rowPage = document.createElement('div');
    rowPage.className = 'abp-row';
    const pageLeft = document.createElement('span'); pageLeft.textContent = 'Page Ads Blocked';
    lblPage = document.createElement('span'); lblPage.id = 'abp-page'; lblPage.textContent = '0';
    rowPage.appendChild(pageLeft); rowPage.appendChild(lblPage);

    const rowTotal = document.createElement('div');
    rowTotal.className = 'abp-row';
    const totalLeft = document.createElement('span'); totalLeft.textContent = 'Total Ads Blocked';
    lblTotal = document.createElement('span'); lblTotal.id = 'abp-total'; lblTotal.textContent = String(totalAds);
    rowTotal.appendChild(totalLeft); rowTotal.appendChild(lblTotal);

    const rowPause = document.createElement('div');
    rowPause.className = 'abp-row';
    const pauseLeft = document.createElement('span'); pauseLeft.textContent = 'Blocking';
    btnPause = document.createElement('span'); btnPause.id = 'abp-pause'; btnPause.className = 'abp-btn';
    btnPause.textContent = isPaused ? '▶ Resume' : '⏸ Pause';
    rowPause.appendChild(pauseLeft); rowPause.appendChild(btnPause);

    body.appendChild(rowPage);
    body.appendChild(rowTotal);
    body.appendChild(rowPause);

    header.appendChild(icon);
    header.appendChild(title);
    panel.appendChild(header);
    panel.appendChild(body);
    document.documentElement.appendChild(panel);

    // restore collapsed state
    if (!isOpen) body.classList.add('collapsed');

    // Drag (header holds drag; click toggles when not moved)
    header.addEventListener('mousedown', (e) => {
      dragActive = true; dragMoved = false;
      const rect = panel.getBoundingClientRect();
      dragDX = e.clientX - rect.left; dragDY = e.clientY - rect.top;
      e.preventDefault();
    });
    document.addEventListener('mousemove', (e) => {
      if (!dragActive) return;
      const left = e.clientX - dragDX, top = e.clientY - dragDY;
      if (Math.abs(left - panel.offsetLeft) > 3 || Math.abs(top - panel.offsetTop) > 3) dragMoved = true;
      panel.style.left = left + 'px';
      panel.style.top  = top + 'px';
      panel.style.right = 'auto';
    });
    document.addEventListener('mouseup', () => { dragActive = false; });

    // Toggle (only if no drag)
    header.addEventListener('click', () => {
      if (dragMoved) return;
      isOpen = !isOpen;
      GM_setValue(KEY_OPEN, isOpen);
      body.classList.toggle('collapsed', !isOpen);
    });

    // Pause
    btnPause.addEventListener('click', (e) => {
      e.stopPropagation();
      isPaused = !isPaused;
      GM_setValue(KEY_PAUSE, isPaused);
      btnPause.textContent = isPaused ? '▶ Resume' : '⏸ Pause';
    });
  }

  // === Helpers (usable in all frames) ===
  const SAFE_TAGS = new Set(['HEADER','NAV','FOOTER']);
  const SAFE_KEYWORDS = ['masthead','topbar','site-header','navbar','menu','breadcrumb','logo','search'];

  // Regex rules (class/id) — case-insensitive
  const RX_PATTERNS = [
      /advert/i,
      /reklam/i,
   // /^ad-.*/i,   // ad-*
   // /.*-ad$/i,   // -ad*
   // /^ad_.*/i,   // ad_*
   // /.*_ad$/i    // *_ad
  ];
  // Exact words via regex (but not 'ad'): Ads / Adverts (case-insensitive)
  const RX_WORDS = /^(ads|adverts)$/i;

  function isPanelOrInside(node) {
    if (!node || node.nodeType !== 1) return false;
    return !!(IS_TOP && panel && (node === panel || node.closest && node.closest('#abp-panel')));
  }

  function isSafeRegion(el) {
    if (!el || el.nodeType !== 1) return false;
    if (SAFE_TAGS.has(el.tagName)) return true;
    const idc = ((el.id || '') + ' ' + (el.className || '')).toLowerCase();
    return SAFE_KEYWORDS.some(k => idc.includes(k));
  }

  function classOrIdMatches(el) {
    // skip if no id/class
    const id = el.id || '';
    const classes = el.classList ? Array.from(el.classList) : [];

    // 1) exact 'ad' (lowercase only), non-regex
    if (id === 'ad') return true;
    if (classes.includes('ad')) return true;

    // 2) Ads / Adverts (case-insensitive, exact) with regex
    if (RX_WORDS.test(id)) return true;
    if (classes.some(c => RX_WORDS.test(c))) return true;

    // 3) pattern regexes
    if (RX_PATTERNS.some(rx => rx.test(id))) return true;
    if (classes.some(c => RX_PATTERNS.some(rx => rx.test(c)))) return true;

    return false;
  }

  const CANDIDATE_SELECTOR = '[id], [class]';

  function scanAndBlock(root) {
    // pull latest pause in frames so top-frame toggle propagates
    isPaused = !!GM_getValue(KEY_PAUSE, false);
    if (isPaused) return;

    const candidates = [];
    if (root.nodeType === 1 && (root.id || root.className)) candidates.push(root);
    if (root.querySelectorAll) candidates.push(...root.querySelectorAll(CANDIDATE_SELECTOR));

    let removedThisRun = 0;

    for (const el of candidates) {
      if (!el || el.nodeType !== 1) continue;
      if (isPanelOrInside(el)) continue;           // never touch the panel
      if (isSafeRegion(el)) continue;              // do not remove safe structural regions
      if (!classOrIdMatches(el)) continue;

      el.remove();
      pageAds++; totalAds++; removedThisRun++;
    }

    if (removedThisRun > 0) {
      GM_setValue(KEY_TOTAL, totalAds);
      // update counters if panel exists
      if (IS_TOP) {
        const pageEl  = document.getElementById('abp-page');
        const totalEl = document.getElementById('abp-total');
        if (pageEl)  pageEl.textContent  = String(pageAds);
        if (totalEl) totalEl.textContent = String(totalAds);
      }
    }
  }

  // Initial sweep + Observer + periodic backup
  const runInitial = () => scanAndBlock(document.documentElement || document.body || document);

  // MutationObserver in all frames
  const mo = new MutationObserver(muts => {
    for (const m of muts) {
      for (const node of m.addedNodes) {
        if (node && node.nodeType === 1) scanAndBlock(node);
      }
    }
  });

  if (document.body) {
    runInitial();
    mo.observe(document.body, { childList: true, subtree: true });
  } else {
    // in very early stages, wait for body
    const ready = new MutationObserver(() => {
      if (document.body) {
        ready.disconnect();
        runInitial();
        mo.observe(document.body, { childList: true, subtree: true });
      }
    });
    ready.observe(document.documentElement, { childList: true, subtree: true });
  }

  // backup sweep (covers SPAs / missed nodes)
  setInterval(() => scanAndBlock(document.body || document), 3000);
})();