eMAG Cleaner

Only shows the products sold and delivered by eMag

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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://greasyfork.org/users/877912
// @version     1.0.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);

// unsafeWindow is the real page window — required because @grant isolates this
// script in a sandbox where window.fetch and EM are not the page's own globals.
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];
log('Detected language:', lang);

// --- State ---

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

// Product id → item map built from API/page globals
let emMap = null;

log('Restored state — filterThirdParty:', filterThirdParty, '| filterPromoted:', filterPromoted, '| collapsed:', collapsed);

// --- Fetch interceptor -------------------------------------------------------
// Must patch pageWindow.fetch (the real page fetch), not the sandboxed window.fetch.
// Without unsafeWindow this patch was silently a no-op and API data was never captured.

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

// --- Page globals ------------------------------------------------------------
// EM.listingGlobals is set inline by eMAG's own page scripts. Must read it via
// pageWindow — accessing bare `EM` from the sandbox would always be undefined.

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');

// Apply restored collapsed state without transition
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);
    log('Saved 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);
    log('Saved filterThirdParty:', filterThirdParty);
    applyFilters();
});

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

// --- Core logic --------------------------------------------------------------

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) {
    // Primary: emMap from API/page globals — works in both grid and list view
    if (emMap) {
        const id = getProductId(card);
        if (id !== null) {
            const vendor = emMap.get(id)?.offer?.vendor?.name?.default;
            if (vendor) return vendor;
        }
    }
    // Fallback: vendor link only rendered in list view
    const vendorLink = card.querySelector('.card-vendor a');
    if (vendorLink) return vendorLink.textContent.trim();

    return null; // unknown — don't hide
}

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

function applyFilters() {
    const cards = document.querySelectorAll('.card-item.card-standard.js-product-data');
    log(`Filtering: found ${cards.length} card(s).`);

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

        if (filterThirdParty) {
            const vendor = getVendor(card);
            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();

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