eMAG Cleaner

Only shows the products sold and delivered by eMag

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==

// @name        eMAG Cleaner
// @name:ro     eMAG Curățător
// @name:bg     eMAG Почистващ
// @name:hu     eMAG Tisztító

// @description    Only shows the products sold and delivered by eMag
// @description:ro Afișează doar produsele vândute și livrate de eMAG
// @description:bg Показва само продуктите, продавани и доставяни от eMAG
// @description:hu Csak az eMAG által értékesített és szállított termékeket jeleníti meg

// @author      NWP + scumpisor
// @namespace   https://greatest.deepsurf.us/users/877912
// @version     1.1.0
// @license     MIT

// @match       *://*.emag.ro/*
// @match       *://*.emag.hu/*
// @match       *://*.emag.bg/*

// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       unsafeWindow
// ==/UserScript==

const DEBUG = false;
const log = (...args) => DEBUG && console.log('[eMag Filter]', ...args);

log('Script loaded on:', window.location.href);

const pageWindow = unsafeWindow;

// --- i18n ---

const STRINGS = {
    en: {
        title:        '🔧 eMAG Cleaner',
        subtitle:     'Configure filtering options:',
        filterThird:  'Filter third-party products',
        filterPromo:  'Filter promoted products',
        expand:       'Expand',
        collapse:     'Collapse',
    },
    ro: {
        title:        '🔧 eMAG Curățător',
        subtitle:     'Configurează opțiunile de filtrare:',
        filterThird:  'Filtrează produsele vândute de terți',
        filterPromo:  'Filtrează produsele promovate',
        expand:       'Extinde',
        collapse:     'Restrânge',
    },
    bg: {
        title:        '🔧 eMAG Почистващ',
        subtitle:     'Конфигурирайте опциите за филтриране:',
        filterThird:  'Филтрирай продукти от трети страни',
        filterPromo:  'Филтрирай промотирани продукти',
        expand:       'Разгъни',
        collapse:     'Свий',
    },
    hu: {
        title:        '🔧 eMAG Tisztító',
        subtitle:     'Szűrési beállítások konfigurálása:',
        filterThird:  'Harmadik féltől származó termékek szűrése',
        filterPromo:  'Hirdetett termékek szűrése',
        expand:       'Kinyitás',
        collapse:     'Bezárás',
    },
};

function detectLang() {
    const raw = (navigator.language || navigator.userLanguage || 'en').toLowerCase().split('-')[0];
    return STRINGS[raw] ? raw : 'en';
}

const lang = detectLang();
const t    = STRINGS[lang];

// --- State ---

let filterThirdParty = GM_getValue('filterThirdParty', true);
let filterPromoted   = GM_getValue('filterPromoted', true);
let collapsed        = GM_getValue('collapsed', false);

let emMap = null;

// --- Fetch interceptor ---

const originalFetch = pageWindow.fetch;
pageWindow.fetch = function (...args) {
    const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
    log('fetch →', url);
    if (typeof url === 'string' && (url.includes('/search-by-url') || url.includes('/search-by-filters'))) {
        return originalFetch.apply(this, args).then(response => {
            response.clone().json().then(data => {
                if (data?.data?.items) {
                    buildMap(data.data.items);
                    log('Built emMap from fetch response, size:', emMap.size);
                    applyFilters();
                }
            }).catch(() => {});
            return response;
        });
    }
    return originalFetch.apply(this, args);
};

// Also intercept XHR — eMAG may use XMLHttpRequest instead of fetch
// for some or all search/filter navigations.
const _origOpen = pageWindow.XMLHttpRequest.prototype.open;
const _origSend = pageWindow.XMLHttpRequest.prototype.send;

pageWindow.XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this._emagUrl = url;
    log('XHR →', method, url);
    return _origOpen.call(this, method, url, ...rest);
};

pageWindow.XMLHttpRequest.prototype.send = function (...args) {
    if (typeof this._emagUrl === 'string' && this._emagUrl.includes('/search-by-url')) {
        this.addEventListener('load', () => {
            try {
                const data = JSON.parse(this.responseText);
                if (data?.data?.items) {
                    buildMap(data.data.items);
                    log('Built emMap from XHR response, size:', emMap.size);
                    applyFilters();
                }
            } catch (_) {}
        });
    }
    return _origSend.apply(this, args);
};

// --- Page globals ---

function tryPageGlobals() {
    if (typeof pageWindow.EM !== 'undefined' && pageWindow.EM?.listingGlobals?.items) {
        buildMap(pageWindow.EM.listingGlobals.items);
        log('Built emMap from page globals, size:', emMap.size);
    }
}

function buildMap(items) {
    emMap = new Map(items.map(item => [item.id, item]));
}



// --- Styles ---

GM_addStyle(`
  #emag-cleaner-panel {
    position: fixed;
    bottom: 2.25rem;
    right: 2.25rem;
    z-index: 999999;
    background: linear-gradient(135deg, #6a5acd 0%, #4a90d9 100%);
    border-radius: 1.5rem;
    padding: 1.65rem 1.65rem 1.35rem 1.65rem;
    width: 25.5rem;
    box-shadow: 0 0.75rem 3rem rgba(0,0,0,0.35);
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    font-size: 1.5rem;
    color: white;
    user-select: none;
    box-sizing: border-box;
  }
  #emag-cleaner-panel h3 {
    margin: 0;
    font-size: 1.425rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    white-space: nowrap;
    cursor: pointer;
    border-radius: 0.6rem;
    padding: 0.2rem 0.3rem;
    transition: background 0.2s;
  }
  #emag-cleaner-panel h3:hover {
    background: rgba(255,255,255,0.1);
  }
  #emag-cleaner-collapse-btn {
    background: rgba(255,255,255,0.3);
    border: 0.2rem solid rgba(255,255,255,0.6);
    border-radius: 0.6rem;
    color: white;
    font-size: 1.05rem;
    font-weight: 900;
    line-height: 1;
    width: 2.4rem;
    height: 2.4rem;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background 0.2s, transform 0.3s;
    padding: 0;
    pointer-events: none;
    cursor: pointer;
  }
  #emag-cleaner-collapse-btn.collapsed {
    transform: rotate(180deg);
  }
  #emag-cleaner-body {
    overflow: hidden;
    transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease;
    max-height: 18rem;
    opacity: 1;
    margin-top: 1.35rem;
  }
  #emag-cleaner-body.collapsed {
    max-height: 0;
    opacity: 0;
    margin-top: 0;
  }
  #emag-cleaner-panel .subtitle {
    font-size: 1.08rem;
    text-align: center;
    opacity: 0.85;
    margin-bottom: 1.35rem;
  }
  #emag-cleaner-panel .toggle-row {
    background: #1e1e2e;
    border-radius: 1.125rem;
    padding: 1.2rem 1.35rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.75rem;
    font-size: 1.125rem;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
  }
  #emag-cleaner-panel .toggle-row:hover {
    background: #2a2a3e;
  }
  #emag-cleaner-panel .toggle-row:last-child {
    margin-bottom: 0;
  }
  #emag-cleaner-panel .toggle {
    position: relative;
    width: 3.9rem;
    height: 2.25rem;
    flex-shrink: 0;
    margin-left: 0.9rem;
  }
  #emag-cleaner-panel .toggle input {
    opacity: 0;
    width: 0;
    height: 0;
  }
  #emag-cleaner-panel .slider {
    position: absolute;
    inset: 0;
    background: #555;
    border-radius: 2.25rem;
    transition: background 0.25s;
    cursor: pointer;
  }
  #emag-cleaner-panel .slider:before {
    content: '';
    position: absolute;
    width: 1.65rem;
    height: 1.65rem;
    left: 0.3rem;
    top: 0.3rem;
    background: white;
    border-radius: 50%;
    transition: transform 0.25s;
  }
  #emag-cleaner-panel .toggle input:checked + .slider {
    background: #22c55e;
  }
  #emag-cleaner-panel .toggle input:checked + .slider:before {
    transform: translateX(1.65rem);
  }
`);

// --- DOM ---

const panel = document.createElement('div');
panel.id = 'emag-cleaner-panel';
panel.innerHTML = `
  <h3 id="emag-cleaner-header">
    <button id="emag-cleaner-collapse-btn" title="${collapsed ? t.expand : t.collapse}">▼</button>
    <span>${t.title}</span>
  </h3>
  <div id="emag-cleaner-body">
    <div class="subtitle">${t.subtitle}</div>

    <label class="toggle-row">
      <span>${t.filterThird}</span>
      <div class="toggle">
        <input type="checkbox" id="toggle-third-party" ${filterThirdParty ? 'checked' : ''}>
        <span class="slider"></span>
      </div>
    </label>

    <label class="toggle-row">
      <span>${t.filterPromo}</span>
      <div class="toggle">
        <input type="checkbox" id="toggle-promoted" ${filterPromoted ? 'checked' : ''}>
        <span class="slider"></span>
      </div>
    </label>
  </div>
`;

document.body.appendChild(panel);

const header      = document.getElementById('emag-cleaner-header');
const collapseBtn = document.getElementById('emag-cleaner-collapse-btn');
const body        = document.getElementById('emag-cleaner-body');

body.style.transition        = 'none';
collapseBtn.style.transition = 'none';
if (collapsed) {
    body.classList.add('collapsed');
    collapseBtn.classList.add('collapsed');
}
requestAnimationFrame(() => {
    body.style.transition        = '';
    collapseBtn.style.transition = '';
});

function toggleCollapse() {
    collapsed = !collapsed;
    GM_setValue('collapsed', collapsed);
    body.classList.toggle('collapsed', collapsed);
    collapseBtn.classList.toggle('collapsed', collapsed);
    collapseBtn.title = collapsed ? t.expand : t.collapse;
}

header.addEventListener('click', toggleCollapse);

document.getElementById('toggle-third-party').addEventListener('change', e => {
    filterThirdParty = e.target.checked;
    GM_setValue('filterThirdParty', filterThirdParty);
    applyFilters();
});

document.getElementById('toggle-promoted').addEventListener('change', e => {
    filterPromoted = e.target.checked;
    GM_setValue('filterPromoted', filterPromoted);
    applyFilters();
});

// --- Core logic ---

// Matches both category/filter pages (.card-standard) and search results pages.
const CARD_SEL = '.card-item.js-product-data';

function getProductId(card) {
    const directId = card.getAttribute('data-product-id');
    if (directId) return parseInt(directId);
    try {
        const raw = card.querySelector('button.add-to-favorites')
                        .getAttribute('data-product')
                        .replace(/&quot;/g, '"');
        return parseInt(JSON.parse(raw).productid);
    } catch (_) {
        return null;
    }
}

function getVendor(card) {
    if (emMap) {
        const id = getProductId(card);
        if (id !== null) {
            const vendor = emMap.get(id)?.offer?.vendor?.name?.default;
            if (vendor) return vendor;
        }
    }
    const vendorLink = card.querySelector('.card-vendor a');
    if (vendorLink) return vendorLink.textContent.trim();
    return null;
}

function isPromoted(card) {
    return !!card.querySelector('span.badge.bg-light.bg-opacity-90');
}

function applyFilters() {
    const cards = document.querySelectorAll(CARD_SEL);
    log(`applyFilters — ${cards.length} card(s), emMap size: ${emMap?.size ?? 'null'}`);

    let hidden = 0;
    cards.forEach(card => {
        card.style.display = '';
        let hide = false;

        if (filterThirdParty) {
            const vendor = getVendor(card);
            const id = card.getAttribute('data-product-id');
            log(`  card ${id} → vendor: ${vendor ?? 'null'}`);
            if (vendor && vendor !== 'eMAG') hide = true;
        }

        if (!hide && filterPromoted && isPromoted(card)) {
            hide = true;
        }

        if (hide) {
            card.style.display = 'none';
            hidden++;
        }
    });

    log(`Hid ${hidden} of ${cards.length} cards.`);
}

// --- Init ---

tryPageGlobals();
applyFilters();

// Debounce to avoid thrashing during large DOM updates (SPA transitions).
let _applyTimer = null;
function scheduleApply() {
    clearTimeout(_applyTimer);
    _applyTimer = setTimeout(applyFilters, 150);
}

// On SPA navigation: clear stale emMap, then poll EM.listingGlobals until
// eMAG's own scripts populate it with the new page's products.
// This covers cases where neither fetch nor XHR intercept fires
// (e.g. the page reuses a cached response or uses a different transport).
let _pollTimer = null;

function onNavigate() {
    log('Navigation detected — clearing emMap');
    emMap = null;
    clearInterval(_pollTimer);

    let attempts = 0;

    _pollTimer = setInterval(() => {
        attempts++;
        const items = pageWindow.EM?.listingGlobals?.items;
        log(`Poll #${attempts} — EM.listingGlobals items:`, items?.length ?? 'none', '| first id:', items?.[0]?.id ?? 'n/a');
        if (items?.length) {
            // Build a temporary map and check how many current DOM cards are covered.
            // If the globals are stale (previous page), most card IDs won't match.
            const tmpMap = new Map(items.map(item => [item.id, item]));
            const domCards = [...document.querySelectorAll(CARD_SEL)];
            const matched  = domCards.filter(c => tmpMap.has(parseInt(c.getAttribute('data-product-id')))).length;
            const coverage = domCards.length ? matched / domCards.length : 0;
            log(`  coverage: ${matched}/${domCards.length} (${(coverage*100).toFixed(0)}%)`);
            if (coverage >= 0.7) { // at least 70% of visible cards are in globals → fresh
                clearInterval(_pollTimer);
                emMap = tmpMap;
                log('Built emMap from polled EM globals after navigation, size:', emMap.size);
                applyFilters();
                return;
            }
        }
        if (attempts >= 40) {
            clearInterval(_pollTimer);
            log('Polling gave up — no fresh EM globals found');
            log('Cards in DOM:', document.querySelectorAll(CARD_SEL).length);
        }
    }, 100);
}

const _origPush    = pageWindow.history.pushState.bind(pageWindow.history);
const _origReplace = pageWindow.history.replaceState.bind(pageWindow.history);
pageWindow.history.pushState    = (...a) => { _origPush(...a);    onNavigate(); };
pageWindow.history.replaceState = (...a) => { _origReplace(...a); onNavigate(); };
pageWindow.addEventListener('popstate', onNavigate);

const observer = new MutationObserver(scheduleApply);
observer.observe(document.body, { childList: true, subtree: true });
log('MutationObserver started.');