eMAG Cleaner

Only shows the products sold and delivered by eMag

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.');