EGS Library RU

Отображение информации на карточках о владении на сайте Epic Games.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         EGS Library RU
// @namespace    http://tampermonkey.net/
// @version      6.4
// @description  Отображение информации на карточках о владении на сайте Epic Games.
// @author       pumPCin
// @license      MIT
// @match        https://store.epicgames.com/*
// @grant        GM.xmlHttpRequest
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.listValues
// @grant        GM.deleteValue
// @connect      store.epicgames.com
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    let lastPath = window.location.href;
    let queue = [];
    let isProcessing = false;
    let isPaused = false;

    const DELAY_LIVE = 3000;
    const OWNED_REGEX = /(?:В библиотеке)/i;

    function normalizeUrl(url) {
        try {
            let u = new URL(url);
            return u.pathname.toLowerCase().replace(/\/+$/, '').split('/').pop() || url;
        } catch (e) { return url; }
    }

    const logWrapper = document.createElement('div');
    logWrapper.id = 'log-wrapper-egs';
    logWrapper.style.cssText = 'position:fixed;bottom:10px;right:10px;width:380px;background:rgba(10,10,10,0.95);color:#0f0;font-family:monospace;font-size:10px;z-index:10000;border:1px solid #333;border-radius:3px;box-shadow:0 0 10px #000;display:flex;flex-direction:column;';

    const btnStyle = 'background:none;border:none;color:#0078f2;cursor:pointer;font-family:monospace;font-size:11px;font-weight:bold;padding:0 2px;';

    logWrapper.innerHTML = `
        <div id="log-h" style="padding:5px;background:#222;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #333;user-select:none;">
            <div style="display:flex;gap:5px;">
                <button id="btn-clear-all" title="Удалить из кэша 'ВСЁ'" style="${btnStyle}color:#f44336;">[К]</button>
                <button id="btn-clear-owned" title="Удалить из кэша 'В БИБЛИОТЕКЕ'" style="${btnStyle}color:#0078f2;">[В]</button>
                <button id="btn-clear-not" title="Удалить из кэша 'НЕ КУПЛЕНО'" style="${btnStyle}color:#ff9800;">[Н]</button>
                <button id="btn-clear-dupes" title="Удалить из кэша дубликаты и конфликты" style="${btnStyle}color:#0ff;">[Д]</button>
            </div>
            <div style="display:flex;gap:10px;align-items:center;">
                <span style="color:#666;">Панель логов EGS Library RU</span>
                <button id="log-t" style="${btnStyle}color:#fff;">[—]</button>
            </div>
        </div>
        <div id="log-b" style="padding:5px;max-height:200px;overflow-y:auto;user-select:text;display:flex;flex-direction:column;"></div>
    `;
    document.body.appendChild(logWrapper);

    const logBody = document.getElementById('log-b');
    const toggleBtn = document.getElementById('log-t');

    toggleBtn.onclick = () => {
        const isH = logBody.style.display === 'none';
        logBody.style.display = isH ? 'flex' : 'none';
        toggleBtn.innerText = isH ? '[—]' : '[+]';
    };

    function addLog(msg, color = '#aaa') {
        const e = document.createElement('div');
        e.style.color = color; e.style.borderBottom = '1px solid #222'; e.style.padding = '2px 0';
        e.innerHTML = `[${new Date().toLocaleTimeString()}] ${msg}`;
        logBody.appendChild(e);
        logBody.scrollTop = logBody.scrollHeight;
        if (logBody.childNodes.length > 50) logBody.firstChild.remove();
    }

    async function clearCache(filter) {
        const keys = await GM.listValues();
        for (const key of keys) {
            const val = await GM.getValue(key);
            if (filter === 'ALL' || (val && val.status === filter)) await GM.deleteValue(key);
        }
        location.reload();
    }

    async function clearDuplicates() {
        addLog("Сканирование кэша…", "#fff");
        const keys = await GM.listValues();
        const seen = {};
        let count = 0;

        for (const key of keys) {
            const val = await GM.getValue(key);
            if (!val || !val.status) continue;

            if (seen[key]) {
                if (seen[key] !== val.status) {
                    addLog(`Конфликт (${key}): удален`, "#fff");
                    await GM.deleteValue(key);
                } else {
                    addLog(`Дубликат (${key}): удален`, "#fff");
                    count++;
                }
            } else { seen[key] = val.status; }
        }
        addLog(`Правка кэша завершена. Удалено записей: ${count}`, "#fff");
    }

    document.getElementById('btn-clear-all').onclick = () => clearCache('ALL');
    document.getElementById('btn-clear-owned').onclick = () => clearCache('OWNED');
    document.getElementById('btn-clear-not').onclick = () => clearCache('NOT_OWNED');
    document.getElementById('btn-clear-dupes').onclick = clearDuplicates;

    async function checkCurrentPage() {
        if (!window.location.pathname.includes('/p/') && !window.location.pathname.includes('/bundles/')) return;

        const gameKey = normalizeUrl(window.location.href);
        const buyButton = document.querySelector('aside') || document.querySelector('[data-testid="purchase-cta-button"]');

        if (buyButton) {
            const isOwned = OWNED_REGEX.test(buyButton.innerText);
            const status = isOwned ? 'OWNED' : 'NOT_OWNED';
            const oldData = await GM.getValue(gameKey);

            if (!oldData || oldData.status !== status) {
                await GM.setValue(gameKey, { status, time: Date.now() });
                addLog(`[СТРАНИЦА ТОВАРА] КЭШ обновлен: ${gameKey} -> ${status}`, "#0ff");
            }
        }
    }

    async function processQueue() {
        if (isProcessing || queue.length === 0) return;
        isProcessing = true;

        while (queue.length > 0) {
            if (isPaused) { await new Promise(r => setTimeout(r, 1000)); continue; }
            const item = queue.shift();
            const gameKey = normalizeUrl(item.url);

            addLog(`LIVE запрос: ${gameKey}`, '#fff');

            await new Promise(resolve => {
                GM.xmlHttpRequest({
                    method: "GET", url: item.url,
                    onload: async (res) => {
                        if (res.status > 200) {
                            addLog(`Ошибка ${res.status}: пауза 60 сек…`, '#fff');;
                            isPaused = true;
                            setTimeout(() => { isPaused = false; addLog('Пауза снята', '#fff'); }, 60000);
                            resolve(); return;
                        }
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(res.responseText, 'text/html');
                        const buyButton = doc.querySelector('aside') || doc.querySelector('[data-testid="purchase-cta-button"]') || doc.body;
                        const isOwned = OWNED_REGEX.test(buyButton.innerText);

                        const status = isOwned ? 'OWNED' : 'NOT_OWNED';
                        await GM.setValue(gameKey, { status, time: Date.now() });
                        applyBadge(item.card, status, false, gameKey);
                        addLog(`LIVE [${status}]: ${gameKey}`, isOwned ? '#4caf50' : '#f44336');
                        resolve();
                    },
                    onerror: () => resolve()
                });
            });
            await new Promise(r => setTimeout(r, DELAY_LIVE));
        }
        isProcessing = false;
    }

    function applyBadge(card, status, isCache, gameKey) {
        const old = card.querySelector('.egs-badge'); if (old) old.remove();
        const badge = document.createElement('div');
        badge.className = 'egs-badge';
        const isOwned = status === 'OWNED';
        const bg = isOwned ? (isCache ? '#0078f2' : '#4caf50') : (isCache ? '#ff9800' : '#f44336');

        badge.style.cssText = `
            position:absolute;
            top:0px;
            left:2px;
            background:${bg};
            color:white;
            padding:3px 8px;
            font-size:10px;
            font-weight:bold;
            border-radius:3px;
            z-index:20;
            pointer-events:none;
            box-shadow:0 2px 4px rgba(0,0,0,0.5);
        `;
        badge.innerText = isOwned ? "В БИБЛИОТЕКЕ" : "НЕ КУПЛЕНО";

        card.style.position = "relative";
        card.appendChild(badge);
        card.dataset.marked = status;
    }

    function scan() {
        if (window.location.href !== lastPath) {
            lastPath = window.location.href;
            queue = [];
            addLog('Смена URL: очистка', '#0ff');
            document.querySelectorAll('.egs-badge').forEach(b => b.remove());
            document.querySelectorAll('a[href*="/p/"], a[href*="/bundles/"]').forEach(l => { 
                delete l.dataset.marked; 
                delete l.dataset.enqueued; 
            });
        }

        checkCurrentPage();

        const links = document.querySelectorAll('a[href*="/p/"], a[href*="/bundles/"]');
        links.forEach(async (link) => {
            const hasImage = link.querySelector('img') || link.querySelector('picture') || link.innerHTML.includes('srcset');
            if (!hasImage) return;

            const gameKey = normalizeUrl(link.href);
            const cached = await GM.getValue(gameKey);

            if (cached) {
                if (link.dataset.marked !== cached.status) {
                    applyBadge(link, cached.status, true, gameKey);
                    addLog(`КЭШ [${cached.status}]: ${gameKey}`, cached.status === 'OWNED' ? '#0078f2' : '#ff9800');
                }
            } else if (!link.dataset.enqueued && !link.dataset.marked) {
                link.dataset.enqueued = "true";
                queue.push({url: link.href, card: link});
                processQueue();
            }
        });
    }

    setInterval(scan, 2500);
    addLog('Скрипт работает', '#0078f2');
})();