Приховати/Показати оголошення OLX

Додає кнопки «Приховати оголошення / Показати оголошення» до списків OLX і запам'ятовує приховані оголошення

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==

// @name         Hide/Show OLX Offers
// @name:ro      Ascunde/Arată ofertele OLX
// @name:bg      Скрий/Покажи обяви в OLX
// @name:uk      Приховати/Показати оголошення OLX
// @name:pt      Ocultar/Mostrar anúncios OLX
// @name:pl      Ukryj/Pokaż ogłoszenia OLX
//
// @description     Adds "Hide offer / Show offer" buttons to OLX listing and remembers hidden offers
// @description:ro  Adaugă butoane „Ascunde oferta / Arată oferta" în listările OLX și reține ofertele ascunse
// @description:bg  Добавя бутони „Скрий обявата / Покажи обявата" към обявите в OLX și запомня скритите обяви
// @description:uk  Додає кнопки «Приховати оголошення / Показати оголошення» до списків OLX і запам'ятовує приховані оголошення
// @description:pt  Adiciona botões "Ocultar anúncio / Mostrar anúncio" às listagens do OLX e memoriza os anúncios ocultos
// @description:pl  Dodaje przyciski „Ukryj ogłoszenie / Pokaż ogłoszenie" do list ogłoszeń OLX i zapamiętuje ukryte ogłoszenia
//
// @author       NWP
// @version      1.4.0
// @license      MIT
//
// @match        *://www.olx.ro/*
// @match        *://www.olx.bg/*
// @match        *://www.olx.ua/*
// @match        *://www.olx.pt/*
// @match        *://www.olx.pl/*
// @run-at       document-idle
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/877912
// ==/UserScript==

(() => {
  "use strict";

  // ===== DEBUG =====
  const DEBUG = false; // set false to silence logs
  const TAG = "[olx-userscript]";
  const dbg = (...a) => DEBUG && console.log(TAG, ...a);
  const dbgBtn = (offerId, action, details) => DEBUG && console.log(TAG, `[BTN:${offerId}]`, action, details || "");

  // ===== Per-language button labels (based on browser language) =====
  const LABELS_BY_LANG = {
    ro: { hide: "Ascunde oferta", show: "Arată oferta" },
    bg: { hide: "Скрий обявата", show: "Покажи обявата" },
    uk: { hide: "Приховати оголошення", show: "Показати оголошення" },
    pt: { hide: "Ocultar anúncio", show: "Mostrar anúncio" },
    pl: { hide: "Ukryj ogłoszenie", show: "Pokaż ogłoszenie" },
  };

  const SUPPORTED_LANGS = new Set(Object.keys(LABELS_BY_LANG));
  const FALLBACK_LABELS = { hide: "Hide offer", show: "Show offer" };

  function detectLabels() {
    const primary = (navigator.language || "").split("-")[0].toLowerCase();
    if (SUPPORTED_LANGS.has(primary)) return LABELS_BY_LANG[primary];
    return FALLBACK_LABELS;
  }

  const LABELS = detectLabels();
  dbg("Labels", LABELS);

  // ===== URL Matching (for SPA navigation) =====
  const ALLOWED_PATHS = [
    '/oferte/',
    '/auto-masini-moto-ambarcatiuni/',
    '/imobiliare/',
    '/locuri-de-munca/',
    '/electronice-si-electrocasnice/',
    '/moda-frumusete/',
    '/piese-auto/',
    '/casa-gradina/',
    '/mama-si-copilul/',
    '/hobby-sport-turism/',
    '/animale-de-companie/',
    '/anunturi-agricole/',
    '/servicii-afaceri-colaborari/',
    '/firme-echipamente-profesionale/',
    '/cazare-turism/',
    '/inchiriere-vehicule-echipamente-articole/'
  ];

  const BLOCKED_PATHS = [
    '/oferte/user/'
  ];

  function isAllowedPage() {
    const path = location.pathname;
    if (BLOCKED_PATHS.some(p => path.startsWith(p))) return false;
    return ALLOWED_PATHS.some(p => path.startsWith(p));
  }

  // ===== SPA Navigation Detection =====
  let lastUrl = location.href;
  const origPushState = history.pushState;
  const origReplaceState = history.replaceState;

  history.pushState = function(...args) {
    dbg('history.pushState called:', args[2]);
    origPushState.apply(this, args);
    onUrlChange();
  };

  history.replaceState = function(...args) {
    dbg('history.replaceState called:', args[2]);
    origReplaceState.apply(this, args);
    onUrlChange();
  };

  window.addEventListener('popstate', () => {
    dbg('popstate event fired');
    onUrlChange();
  });

  function onUrlChange() {
    const currentUrl = location.href;
    if (currentUrl !== lastUrl) {
      dbg('onUrlChange: URL CHANGED from', lastUrl, 'to', currentUrl, 'allowed:', isAllowedPage());
      lastUrl = currentUrl;
      handleVisibility();
    }
  }

  function handleVisibility() {
    const allowed = isAllowedPage();
    dbg('handleVisibility: allowed?', allowed, 'pathname:', location.pathname);

    // Hide or show all injected buttons based on page
    const allBtns = document.querySelectorAll('.olx-ext-btn');
    allBtns.forEach(btn => {
      btn.style.setProperty('display', allowed ? '' : 'none', 'important');
    });

    // Restore hidden cards if we're not on an allowed page
    if (!allowed) {
      const cards = document.querySelectorAll('[data-testid="l-card"].olx-ext-hidden-card');
      cards.forEach(card => card.classList.remove('olx-ext-hidden-card'));
    } else {
      // Re-scan when navigating to an allowed page
      scanAndApply();
    }
  }

  // ===== Storage =====
  const STORAGE_KEY = "olx-ext-hidden";
  const MAX_STATES = 1000;

  // ===== CSS =====
  if (typeof GM_addStyle === "function") {
    GM_addStyle(`
      .olx-ext-btn {
        display: block !important;
        width: calc(100% - 20px) !important;
        margin: 10px !important;
        padding: 12px 14px !important;
        border: 0 !important;
        border-radius: 10px !important;
        cursor: pointer !important;
        font: 14px/1.1 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif !important;
        box-shadow: 0 6px 16px rgba(0,0,0,0.14) !important;
        user-select: none !important;
        text-align: center !important;
      }

      .olx-ext-btn--hide {
        background: #d32f2f !important;
        color: #fff !important;
      }

      .olx-ext-btn--hide:hover {
        background: #b71c1c !important;
      }

      .olx-ext-btn--show {
        background: #2e7d32 !important;
        color: #fff !important;
      }

      .olx-ext-btn--show:hover {
        background: #1b5e20 !important;
      }

      [data-testid="l-card"] {
        display: flex;
        flex-direction: column;
      }
      .olx-ext-btn {
        margin-top: auto !important;
      }

      .olx-ext-hidden-card {
        overflow: hidden !important;
        pointer-events: none !important;
      }
      .olx-ext-hidden-card > :not(.olx-ext-btn) {
        display: none !important;
      }
      .olx-ext-hidden-card .olx-ext-btn {
        pointer-events: auto !important;
        opacity: 1 !important;
        filter: none !important;
      }
    `);
  }

  // ===== Hidden IDs (localStorage) with max 1000 and oldest eviction =====
  function readHiddenList() {
    try {
      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
      if (Array.isArray(raw)) return raw.filter((x) => typeof x === "string");
      return [];
    } catch {
      return [];
    }
  }

  function writeHiddenList(list) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
  }

  function isHidden(id) {
    return readHiddenList().includes(id);
  }

  function addHidden(id) {
    let list = readHiddenList();
    list = list.filter((x) => x !== id);
    list.push(id);
    if (list.length > MAX_STATES) list = list.slice(list.length - MAX_STATES);
    writeHiddenList(list);
    dbg("addHidden", { id, size: list.length });
  }

  function removeHidden(id) {
    const list = readHiddenList().filter((x) => x !== id);
    writeHiddenList(list);
    dbg("removeHidden", { id, size: list.length });
  }

  // ===== Offer ID =====
  function deriveOfferId(card) {
    if (!card) return null;

    if (card.id) return card.id;

    const dataId = card.getAttribute("data-id") || card.dataset?.id;
    if (dataId) return String(dataId);

    const a = card.querySelector('a[href*="/d/oferta/"], a[href*="ID"], a[href]');
    const href = a?.getAttribute("href") || "";
    const m = href.match(/(ID[a-zA-Z0-9]+)/);
    if (m?.[1]) return m[1];

    if (href) return `href:${href.split("?")[0]}`;
    return null;
  }

  function setCardHidden(card, hidden) {
    if (hidden) card.classList.add("olx-ext-hidden-card");
    else card.classList.remove("olx-ext-hidden-card");

    // Also hide/show the .olx-rating-box sibling within the same wrapper
    const wrapper = card.closest('.olx-rating-rowwrap') || card.closest('.olx-rating-cardhost')?.parentElement;
    if (wrapper) {
      const ratingBox = wrapper.querySelector('.olx-rating-box');
      if (ratingBox) {
        ratingBox.style.setProperty('display', hidden ? 'none' : '', 'important');
      }
    }
  }

  function upsertButton(card, offerId) {
    let btn = card.querySelector(`button.olx-ext-btn[data-offer-id="${CSS.escape(offerId)}"]`);

    if (!btn) {
      btn = document.createElement("button");
      btn.type = "button";
      btn.className = "olx-ext-btn custom-button";
      btn.dataset.offerId = offerId;

      card.appendChild(btn);

      btn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        toggleHidden(card, offerId);
      });

      dbgBtn(offerId, "CREATED", "Button inserted into DOM");
    }

    const hidden = isHidden(offerId);
    const currentText = btn.textContent;
    const newText = hidden ? LABELS.show : LABELS.hide;
    const hasShowClass = btn.classList.contains("olx-ext-btn--show");
    const hasHideClass = btn.classList.contains("olx-ext-btn--hide");

    if (currentText !== newText) {
      dbgBtn(offerId, "TEXT CHANGE", `"${currentText}" → "${newText}"`);
      btn.textContent = newText;
    }

    if (hidden && !hasShowClass) {
      dbgBtn(offerId, "CLASS ADD", "olx-ext-btn--show");
      btn.classList.add("olx-ext-btn--show");
    } else if (!hidden && hasShowClass) {
      dbgBtn(offerId, "CLASS REMOVE", "olx-ext-btn--show");
      btn.classList.remove("olx-ext-btn--show");
    }

    if (!hidden && !hasHideClass) {
      dbgBtn(offerId, "CLASS ADD", "olx-ext-btn--hide");
      btn.classList.add("olx-ext-btn--hide");
    } else if (hidden && hasHideClass) {
      dbgBtn(offerId, "CLASS REMOVE", "olx-ext-btn--hide");
      btn.classList.remove("olx-ext-btn--hide");
    }

    return btn;
  }

  function toggleHidden(card, offerId) {
    if (isHidden(offerId)) {
      dbgBtn(offerId, "USER ACTION", "Unhiding offer");
      removeHidden(offerId);
      setCardHidden(card, false);
      dbg("Unhid offer", { offerId });
    } else {
      dbgBtn(offerId, "USER ACTION", "Hiding offer");
      addHidden(offerId);
      setCardHidden(card, true);
      dbg("Hid offer", { offerId });
    }

    upsertButton(card, offerId);
  }

  // ===== Scan =====
  function scanAndApply() {
    if (!isAllowedPage()) {
      dbg("scanAndApply SKIPPED — not on allowed page", location.pathname);
      return;
    }

    dbg("scanAndApply START", { isUpdating });
    isUpdating = true;
    observer.disconnect();
    dbg("Observer disconnected");

    const cards = document.querySelectorAll('[data-testid="l-card"]');
    const hiddenSet = new Set(readHiddenList());
    dbg(`Found ${cards.length} cards, ${hiddenSet.size} hidden IDs`);

    for (const card of cards) {
      const offerId = deriveOfferId(card);
      if (!offerId) continue;

      upsertButton(card, offerId);
      setCardHidden(card, hiddenSet.has(offerId));
    }

    // Use requestAnimationFrame to ensure DOM mutations are processed before resetting flag
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        isUpdating = false;
        observer.observe(document.body, { childList: true, subtree: true });
        dbg("scanAndApply END - Observer reconnected, isUpdating reset");
      });
    });
    dbg("scanAndApply END (observer will reconnect after RAF)");
  }

  // ===== Mutation observer (throttled) =====
  let scheduled = false;
  let isUpdating = false;
  const observer = new MutationObserver((mutations) => {
    // Check for URL changes on every mutation (SPA navigation)
    onUrlChange();

    if (scheduled || isUpdating) {
      dbg("Observer: skipped (scheduled or isUpdating)", { scheduled, isUpdating, mutations: mutations.length });
      return;
    }
    if (!isAllowedPage()) {
      dbg("Observer: skipped (not on allowed page)");
      return;
    }
    dbg("Observer: scheduling scan", { mutations: mutations.length });
    scheduled = true;
    setTimeout(() => {
      scanAndApply();
      scheduled = false;
    }, 250);
  });

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

  // ===== Startup + integrity scan =====
  dbg("Initial scan on startup");
  scanAndApply();
  setInterval(() => {
    dbg("Periodic integrity scan (5s interval)");
    scanAndApply();
  }, 5000);
})();