Додає кнопки «Приховати оголошення / Показати оголошення» до списків OLX і запам'ятовує приховані оголошення
// ==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);
})();