Greasy Fork 还支持 简体中文。

EGS Library RU

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

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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');
})();