Greasy Fork

SteamDB - Sales; Ultimate Enhancer

Комплексное улучшение для SteamDB: фильтры по языкам, спискам, дате, РРЦ, конвертация валют, расширенная информация об играх

当前为 2025-05-26 提交的版本,查看 最新版本

// ==UserScript==
// @name          SteamDB - Sales; Ultimate Enhancer
// @namespace     https://steamdb.info/
// @version       1.2
// @description   Комплексное улучшение для SteamDB: фильтры по языкам, спискам, дате, РРЦ, конвертация валют, расширенная информация об играх
// @author        0wn3df1x
// @license       MIT
// @include       https://steamdb.info/sales/*
// @include       https://steamdb.info/stats/mostfollowed/*
// @grant         GM_xmlhttpRequest
// @grant         GM_setValue
// @grant         GM_getValue
// @connect       api.steampowered.com
// ==/UserScript==

(function() {
    'use strict';

    const scriptsConfig = {
        toggleEnglishLangInfo: false
    };
    const API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1";
    const BATCH_SIZE = 200;
    const HOVER_DELAY = 300;
    const REQUEST_DELAY = 200;
    const DEFAULT_EXCHANGE_RATE = 0.19;

    let collectedAppIds = new Set();
    let tooltip = null;
    let hoverTimer = null;
    let gameData = {};
    let activeLanguageFilter = null;
    let totalGamesOnPage = 0;
    let processedRuGames = 0;
    let processedUsGames = 0;
    let processedSingleStageGames = 0;

    let progressContainer = null;
    let requestQueue = [];
    let isProcessingQueue = false;
    let currentExchangeRate = DEFAULT_EXCHANGE_RATE;
    let activeListFilter = false;
    let activeDateFilterTimestamp = null;
    let isProcessingStarted = false;
    let processButton = null;
    let currentProcessingStage = '';

    let isRuModeActive = false;
    let activeRrcFilters = {
        lower: false,
        equal: false,
        higher: false
    };


    const PROCESS_BUTTON_TEXT = {
        idle: "Обработать игры",
        processing_ru: "Сбор RU данных...",
        processing_us: "Сбор US данных...",
        processing_single: "Сбор данных...",
        done: "Обработка завершена"
    };

    const STATUS_TEXT = {
        select_all_slow: "Выберите 'All (slow)' entries per page и нажмите \"Обработать игры\".",
        processing_ru: "Идет сбор RU данных...",
        processing_us: "Идет сбор US данных...",
        processing_single: "Идет сбор данных...",
        processing_rrc: "Расчет РРЦ...",
        done: "Обработка завершена. Фильтры применены.",
        done_no_rrc: "Обработка данных завершена.",
        rrc_disabled: "РРЦ анализ доступен только для российской валюты.",
        error: "Произошла ошибка."
    };

    const styles = `
    .steamdb-enhancer * { box-sizing: border-box; margin: 0; padding: 0; }
    .steamdb-enhancer { background: #16202d; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); padding: 12px; width: auto; margin-top: 5px; margin-bottom: 15px; max-width: 900px; }
    .enhancer-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; flex-wrap: wrap; }
    .row-layout { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; margin-bottom: 12px; }
    .row-layout.compact { gap: 8px; margin-bottom: 0; }
    .control-group { background: #1a2635; border-radius: 6px; padding: 10px; margin: 6px 0; }
    .group-title { color: #66c0f4; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
    .btn-group { display: flex; flex-wrap: wrap; gap: 5px; }
    .btn { background: #2a3a4d; border: 1px solid #354658; border-radius: 4px; color: #c6d4df; cursor: pointer; font-size: 12px; padding: 5px 10px; transition: all 0.2s ease; display: flex; align-items: center; gap: 5px; white-space: nowrap; }
    .btn:hover { background: #31455b; border-color: #3d526b; }
    .btn.active { background: #66c0f4 !important; border-color: #66c0f4 !important; color: #1b2838 !important; }
    .btn-icon { width: 12px; height: 12px; fill: currentColor; }
    .progress-container { background: #1a2635; border-radius: 4px; height: 6px; overflow: hidden; margin: 10px 0 5px; }
    .progress-text { display: flex; justify-content: space-between; color: #8f98a0; font-size: 11px; margin: 4px 2px 0; }
    .progress-count { flex: 1; text-align: left; }
    .progress-percent { flex: 1; text-align: right; }
    .progress-bar { height: 100%; background: linear-gradient(90deg, #66c0f4 0%, #4d9cff 100%); transition: width 0.3s ease; }
    .converter-group { display: flex; gap: 6px; flex: 1; }
    .input-field { background: #1a2635; border: 1px solid #2a3a4d; border-radius: 4px; color: #c6d4df; font-size: 12px; padding: 5px 8px; min-width: 60px; width: 80px; }
    .date-picker { background: #1a2635; border: 1px solid #2a3a4d; border-radius: 4px; color: #c6d4df; font-size: 12px; padding: 5px; width: 120px; }
    .status-indicator { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 5px 8px; border-radius: 4px; color: #8f98a0;}
    .status-indicator.status-active { color: #66c0f4; }
    .steamdb-tooltip { position: absolute; background: #1b2838; color: #c6d4df; padding: 15px; border-radius: 3px; width: 320px; font-size: 14px; line-height: 1.5; box-shadow: 0 0 12px rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s; pointer-events: none; z-index: 9999; display: none; }
    .tooltip-arrow { position: absolute; width: 0; height: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; }
    .group-top { margin-bottom: 8px; }
    .group-middle { margin-bottom: 12px; }
    .group-bottom { margin-bottom: 15px; }
    .tooltip-row { margin-bottom: 4px; }
    .tooltip-row.compact { margin-bottom: 2px; }
    .tooltip-row.spaced { margin-bottom: 10px; }
    .tooltip-row.language { margin-bottom: 8px; }
    .tooltip-row.description { margin-top: 15px; padding-top: 10px; border-top: 1px solid #2a3a4d; color: #8f98a0; font-style: italic; }
    .positive { color: #66c0f4; }
    .mixed { color: #997a00; }
    .negative { color: #a74343; }
    .no-reviews { color: #929396; }
    .language-yes { color: #66c0f4; }
    .language-no { color: #a74343; }
    .early-access-yes { color: #66c0f4; }
    .early-access-no { color: #929396; }
    .no-data { color: #929396; }
    tr.app td:first-child { position: relative; }

    .rrc-display-container {
        position: absolute;
        width: 100px;
        left: -100px;
        top: -1px;
        height: 100%;
        box-sizing: border-box;

        background-color: var(--body-bg-color, #161920);

        border-top: 1px solid var(--border-color-2, hsl(216, 25%, 16%));
        border-left: none;
        border-right: none;

        border-radius: 0;

        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;

        padding: 0;
        font-size: 11px;
        color: var(--body-color, #ddd);
        text-align: center;
        white-space: normal;
        overflow: hidden;

        z-index: 3;
        pointer-events: none;
    }

    .rrc-display-container .rrc-content-wrapper {
        padding: 1px 3px;
    }

    .rrc-display-container .rrc-text {
        font-weight: 700;
        font-style: normal;
        padding: 1px 5px;
        border-radius: 3px;
        display: inline-block;
        line-height: 1.2;
        margin-bottom: 2px;
    }
    .rrc-display-container .rrc-text.equal {
        background-color: #1566b7 !important;
        color: #d1e5fa !important;
    }
    .rrc-display-container .rrc-text.higher {
        background-color: #cb2431 !important;
        color: #fde2e4 !important;
    }
    .rrc-display-container .rrc-text.lower {
        background-color: #4c6b22 !important;
        color: #c0ef15 !important;
    }

    .rrc-display-container .rrc-details {
        color: var(--muted-color, #999);
        font-size: 10px;
        line-height: 1.1;
        display: block;
    }
    .rrc-display-container .rrc-no-data {
        color: var(--muted-color, #999);
        font-style: italic;
        font-size: 11px;
        padding: 2px 0;
    }
    #rrc-filter-group.disabled-filter {
        opacity: 0.5;
        pointer-events: none;
    }
    #rrc-filter-group.disabled-filter .btn {
        cursor: not-allowed;
    }
    `;

    function isRuCurrencySelected() {
        const currencySelector = document.querySelector('details#js-select-cc');
        if (currencySelector) {
            const checkedRadio = currencySelector.querySelector('input[name="cc"]:checked');
            if (checkedRadio) {
                return checkedRadio.value === 'ru';
            }
            return currencySelector.dataset.default === 'ru';
        }

        const priceHeader = document.querySelector('th[data-name="price"] img[src*="/ru.svg"]');
        if (priceHeader) return true;


        const urlParams = new URLSearchParams(window.location.search);
        if (urlParams.get('cc') === 'ru') return true;

        return false;
    }


    function calculateRecommendedRubPrice(pUSD) {
        if (typeof pUSD !== 'number' || isNaN(pUSD)) return null;
        if (pUSD < 0.99) return 42;
        if (pUSD >= 0.99 && pUSD < 1.99) return 42;
        if (pUSD >= 1.99 && pUSD < 2.99) return 82;
        if (pUSD >= 2.99 && pUSD < 3.99) return 125;
        if (pUSD >= 3.99 && pUSD < 4.99) return 165;
        if (pUSD >= 4.99 && pUSD < 5.99) return 200;
        if (pUSD >= 5.99 && pUSD < 6.99) return 240;
        if (pUSD >= 6.99 && pUSD < 7.99) return 280;
        if (pUSD >= 7.99 && pUSD < 8.99) return 320;
        if (pUSD >= 8.99 && pUSD < 9.99) return 350;
        if (pUSD >= 9.99 && pUSD < 10.99) return 385;
        if (pUSD >= 10.99 && pUSD < 11.99) return 420;
        if (pUSD >= 11.99 && pUSD < 12.99) return 460;
        if (pUSD >= 12.99 && pUSD < 13.99) return 490;
        if (pUSD >= 13.99 && pUSD < 14.99) return 520;
        if (pUSD >= 14.99 && pUSD < 15.99) return 550;
        if (pUSD >= 15.99 && pUSD < 16.99) return 590;
        if (pUSD >= 16.99 && pUSD < 17.99) return 620;
        if (pUSD >= 17.99 && pUSD < 18.99) return 650;
        if (pUSD >= 18.99 && pUSD < 19.99) return 680;
        if (pUSD >= 19.99 && pUSD < 22.99) return 710;
        if (pUSD >= 22.99 && pUSD < 27.99) return 880;
        if (pUSD >= 27.99 && pUSD < 32.99) return 1100;
        if (pUSD >= 32.99 && pUSD < 37.99) return 1200;
        if (pUSD >= 37.99 && pUSD < 43.99) return 1300;
        if (pUSD >= 43.99 && pUSD < 47.99) return 1500;
        if (pUSD >= 47.99 && pUSD < 52.99) return 1600;
        if (pUSD >= 52.99 && pUSD < 57.99) return 1750;
        if (pUSD >= 57.99 && pUSD < 63.99) return 1900;
        if (pUSD >= 63.99 && pUSD < 67.99) return 2100;
        if (pUSD >= 67.99 && pUSD < 74.99) return 2250;
        if (pUSD >= 74.99 && pUSD < 79.99) return 2400;
        if (pUSD >= 79.99 && pUSD < 84.99) return 2600;
        if (pUSD >= 84.99 && pUSD < 89.99) return 2700;
        if (pUSD >= 89.99 && pUSD < 99.99) return 2900;
        if (pUSD >= 99.99 && pUSD < 109.99) return 3200;
        if (pUSD >= 109.99 && pUSD < 119.99) return 3550;
        if (pUSD >= 119.99 && pUSD < 129.99) return 3900;
        if (pUSD >= 129.99 && pUSD < 139.99) return 4200;
        if (pUSD >= 139.99 && pUSD < 149.99) return 4500;
        if (pUSD >= 149.99 && pUSD < 199.99) return 4800;
        if (pUSD >= 199.99) return 6500;
        return null;
    }

    function getPriceInCents(purchaseOption) {
        if (!purchaseOption) return null;
        if (purchaseOption.discount_pct > 0 && purchaseOption.original_price_in_cents) {
            return parseInt(purchaseOption.original_price_in_cents, 10);
        }
        if (purchaseOption.final_price_in_cents) {
            return parseInt(purchaseOption.final_price_in_cents, 10);
        }
        return null;
    }


    function createRrcDisplayElement(appId) {
        const container = document.createElement('div');
        container.className = 'rrc-display-container';
        const data = gameData[appId];
        let rrcStatus = 'no_data';

        let htmlContent = `<div class="rrc-content-wrapper"><span class="rrc-no-data">Нет данных РРЦ</span></div>`;

        if (isRuModeActive && data && typeof data.price_us_initial_cents === 'number' && typeof data.price_ru_initial_cents === 'number') {
            const pUSD = data.price_us_initial_cents / 100;
            const actualRubPrice = data.price_ru_initial_cents / 100;
            const recommendedRubPrice = calculateRecommendedRubPrice(pUSD);

            if (recommendedRubPrice !== null) {
                const diff = actualRubPrice - recommendedRubPrice;
                const diffPercent = recommendedRubPrice !== 0 ? (diff / recommendedRubPrice) * 100 : (diff > 0 ? Infinity : (actualRubPrice === 0 && recommendedRubPrice === 0 ? 0 : -Infinity));

                let textClass = 'equal';
                let symbol = '=';
                if (diff > 0.01) {
                    textClass = 'higher';
                    symbol = '>';
                    rrcStatus = 'higher';
                } else if (diff < -0.01) {
                    textClass = 'lower';
                    symbol = '<';
                    rrcStatus = 'lower';
                } else {
                    rrcStatus = 'equal';
                }

                htmlContent = `
                    <div class="rrc-content-wrapper">
                        <span class="rrc-text ${textClass}">${symbol} РРЦ</span>
                        <span class="rrc-details">(${diffPercent !== Infinity && diffPercent !== -Infinity ? diffPercent.toFixed(0) + '%' : (diffPercent > 0 ? '>~' : '<~') }, ${diff.toFixed(0)} ₽)</span>
                    </div>
                `;
            } else {
                rrcStatus = 'no_rec_price';
                htmlContent = `<div class="rrc-content-wrapper"><span class="rrc-no-data">Нет данных РРЦ (USD?)</span></div>`;
            }
        } else if (isRuModeActive && data && (!data.price_us_initial_cents || !data.price_ru_initial_cents)) {
            rrcStatus = 'no_price_data';
        }

        if (!isRuModeActive) rrcStatus = 'not_applicable';

        if (data) {
            gameData[appId].rrc_status = rrcStatus;
        }
        container.innerHTML = htmlContent;
        return container;
    }

    function injectRrcDisplay(row) {
        if (!isRuModeActive) return;

        const appId = row.dataset.appid;
        if (!appId) return;
        const targetCell = row.querySelector('td:first-child');
        if (!targetCell) return;

        let displayElement = targetCell.querySelector('.rrc-display-container');
        if (displayElement) {
            displayElement.remove();
        }
        displayElement = createRrcDisplayElement(appId);
        targetCell.prepend(displayElement);
    }

    function injectAllRrcDisplays() {
        if (!isRuModeActive) {

            document.querySelectorAll('.rrc-display-container').forEach(el => el.remove());
            return;
        }
        console.log("Injecting RRC displays...");
        document.querySelectorAll('tr.app[data-appid]').forEach(row => {
            injectRrcDisplay(row);
        });
        console.log("RRC displays injected.");
    }


    function createFiltersContainer() {
        const container = document.createElement('div');
        container.className = 'steamdb-enhancer';
        let rrcFilterHTML = `
            <div class="control-group" id="rrc-filter-control-group">
                <div class="group-title">Фильтр РРЦ</div>
                <div class="btn-group" id="rrc-filter-group">
                    <button class="btn" data-filter-rrc="lower" title="Дешевле РРЦ">&lt; РРЦ</button>
                    <button class="btn" data-filter-rrc="equal" title="Соответствует РРЦ">= РРЦ</button>
                    <button class="btn" data-filter-rrc="higher" title="Дороже РРЦ">&gt; РРЦ</button>
                </div>
            </div>`;

        container.innerHTML = `
        <div class="enhancer-header">
            <button class="btn" id="process-btn">
                <svg class="btn-icon" viewBox="0 0 24 24"><path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.8.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/></svg>
                <span id="process-btn-text">${PROCESS_BUTTON_TEXT.idle}</span>
            </button>
            <div class="status-indicator status-inactive">${STATUS_TEXT.select_all_slow}</div>
        </div>
        <div class="progress-container"><div class="progress-bar"></div></div>
        <div class="progress-text">
            <span class="progress-count">0/0</span>
            <span class="progress-percent">(0%)</span>
        </div>
        <div class="row-layout">
            <div class="control-group">
                <div class="group-title">Русский перевод</div>
                <div class="btn-group">
                    <button class="btn" data-filter="russian-any">Только текст</button>
                    <button class="btn" data-filter="russian-audio">Озвучка</button>
                    <button class="btn" data-filter="no-russian">Без перевода</button>
                </div>
            </div>
            <div class="control-group">
                <div class="group-title">Списки</div>
                <div class="btn-group">
                    <button class="btn" data-action="list1">Список 1</button>
                    <button class="btn" data-action="list2">Список 2</button>
                    <button class="btn" data-action="list-filter">Фильтр списков</button>
                </div>
            </div>
            ${isRuModeActive ? rrcFilterHTML : ''}
             <div class="control-group">
                <div class="group-title">Дополнительные инструменты</div>
                <div class="row-layout compact">
                    <div class="converter-group">
                        <input type="number" class="input-field" id="exchange-rate-input" value="${DEFAULT_EXCHANGE_RATE}" step="0.01">
                        <button class="btn" data-action="convert">Конвертировать</button>
                    </div>
                    <div class="btn-group">
                        <input type="date" class="date-picker">
                        <button class="btn" data-action="date-filter">Фильтр по дате</button>
                    </div>
                </div>
            </div>
        </div>`;

        if (!isRuModeActive) {
            const rrcGroup = container.querySelector('#rrc-filter-control-group');
            if (rrcGroup) rrcGroup.classList.add('disabled-filter');
            const statusIndicator = container.querySelector('.status-indicator');
            if (statusIndicator) statusIndicator.textContent = STATUS_TEXT.rrc_disabled;
        }
        return container;
    }

    function handleFilterClick(event) {
        const langBtn = event.target.closest('[data-filter]');
        if (langBtn) {
            const filterType = langBtn.dataset.filter;
            if (filterType.startsWith('russian-') || filterType === 'no-russian') {
                const wasActive = langBtn.classList.contains('active');
                document.querySelectorAll('[data-filter^="russian-"], [data-filter="no-russian"]').forEach(b => b.classList.remove('active'));
                if (!wasActive) {
                    langBtn.classList.add('active');
                    activeLanguageFilter = filterType;
                } else {
                    activeLanguageFilter = null;
                }
                applyAllFilters();
            }
        }

        if (isRuModeActive) {
            const rrcBtn = event.target.closest('[data-filter-rrc]');
            if (rrcBtn) {
                const filterType = rrcBtn.dataset.filterRrc;
                activeRrcFilters[filterType] = !activeRrcFilters[filterType];
                rrcBtn.classList.toggle('active', activeRrcFilters[filterType]);
                applyAllFilters();
            }
        }
    }


    function handleControlClick(event) {
        const btn = event.target.closest('[data-action]');
        if (!btn) return;
        const action = btn.dataset.action;
        switch (action) {
            case 'list1':
                saveList('list1');
                break;
            case 'list2':
                saveList('list2');
                break;
            case 'list-filter':
                activeListFilter = !activeListFilter;
                btn.classList.toggle('active', activeListFilter);
                applyAllFilters();
                break;
            case 'convert':
                currentExchangeRate = parseFloat(document.getElementById('exchange-rate-input').value) || DEFAULT_EXCHANGE_RATE;
                convertPrices();
                break;
            case 'date-filter': {
                const dateInput = btn.previousElementSibling;
                if (btn.classList.contains('active')) {
                    btn.classList.remove('active');
                    dateInput.value = '';
                    activeDateFilterTimestamp = null;
                } else {
                    const selectedDate = dateInput.value;
                    if (selectedDate) {
                        const dateObj = new Date(selectedDate + 'T00:00:00Z');
                        activeDateFilterTimestamp = dateObj.getTime() / 1000;
                        btn.classList.toggle('active', !isNaN(activeDateFilterTimestamp));
                        if (isNaN(activeDateFilterTimestamp)) activeDateFilterTimestamp = null;
                    } else {
                        activeDateFilterTimestamp = null;
                        btn.classList.remove('active');
                    }
                }
                applyAllFilters();
                break;
            }
        }
    }

    function saveList(listName) {
        const appIds = Array.from(collectedAppIds);
        localStorage.setItem(listName, JSON.stringify(appIds));
        alert(`Список ${listName} сохранён (${appIds.length} игр)`);
    }

    function convertPrices() {
        document.querySelectorAll('tr.app').forEach(row => {
            const priceCells = row.querySelectorAll('td.dt-type-numeric');
            if (priceCells.length < 3) return;
            const priceElement = priceCells[2];
            if (!priceElement.dataset.originalPrice) {
                priceElement.dataset.originalPrice = priceElement.textContent.trim();
            }
            const originalPriceText = priceElement.dataset.originalPrice;
            let priceValue = NaN;
            if (originalPriceText.includes('S/.')) {
                const priceMatch = originalPriceText.match(/S\/\.\s*([0-9,.]+)/);
                priceValue = priceMatch ? parseFloat(priceMatch[1].replace(/\s/g, '').replace(',', '.')) : NaN;
            } else if (originalPriceText.includes('₽')) {
                const priceMatch = originalPriceText.match(/([0-9,\s]+)\s*₽/);
                priceValue = priceMatch ? parseFloat(priceMatch[1].replace(/\s/g, '').replace(',', '.')) : NaN;
            } else if (originalPriceText.toLowerCase().includes('free')) {
                priceValue = 0;
            } else {
                const priceMatch = originalPriceText.replace(',', '.').match(/([0-9.]+)/);
                priceValue = priceMatch ? parseFloat(priceMatch[1]) : NaN;
            }

            if (!isNaN(priceValue)) {
                if (priceValue === 0) {
                    priceElement.textContent = priceElement.dataset.originalPrice;
                } else {
                    const converted = (priceValue * currentExchangeRate).toFixed(2);
                    priceElement.textContent = converted;
                }
            } else {
                priceElement.textContent = originalPriceText;
            }
        });
    }

    function applyAllFilters() {
        console.log("Applying filters...");
        const rows = document.querySelectorAll('tr.app');
        const list1 = JSON.parse(localStorage.getItem('list1') || '[]');
        const list2 = JSON.parse(localStorage.getItem('list2') || '[]');
        const commonIds = new Set(list1.filter(id => list2.includes(id)));

        rows.forEach(row => {
            const appId = row.dataset.appid;
            const data = gameData[appId];
            let visible = true;

            if (activeListFilter) {
                visible = !commonIds.has(appId);
            }

            if (visible && activeDateFilterTimestamp !== null) {
                const cells = row.querySelectorAll('.timeago');
                const timeToCheck = parseInt(cells[1]?.dataset.sort || cells[0]?.dataset.sort || '0');
                if (!timeToCheck || isNaN(timeToCheck)) {
                    visible = false;
                } else {
                    visible = timeToCheck >= activeDateFilterTimestamp;
                }
            }

            if (visible && activeLanguageFilter && data) {
                const lang = data.language_support_russian || {};
                switch (activeLanguageFilter) {
                    case 'russian-any':
                        visible = (lang.supported || lang.subtitles) && !lang.full_audio;
                        break;
                    case 'russian-audio':
                        visible = lang.full_audio;
                        break;
                    case 'no-russian':
                        visible = !lang.supported && !lang.full_audio && !lang.subtitles;
                        break;
                }
            } else if (visible && activeLanguageFilter && !data && isProcessingStarted) {
                visible = false;
            }

            if (isRuModeActive && visible) {
                const rrcFilterIsActive = activeRrcFilters.lower || activeRrcFilters.equal || activeRrcFilters.higher;
                const allRrcFiltersSelected = activeRrcFilters.lower && activeRrcFilters.equal && activeRrcFilters.higher;
                const noRrcFiltersSelected = !activeRrcFilters.lower && !activeRrcFilters.equal && !activeRrcFilters.higher;


                if (rrcFilterIsActive && !allRrcFiltersSelected && !noRrcFiltersSelected) {
                    const rrcStatus = data?.rrc_status;
                    if (!rrcStatus || rrcStatus === 'no_data' || rrcStatus === 'no_price_data' || rrcStatus === 'no_rec_price' || rrcStatus === 'not_applicable') {
                        visible = false;
                    } else {
                        let match = false;
                        if (activeRrcFilters.lower && rrcStatus === 'lower') match = true;
                        if (activeRrcFilters.equal && rrcStatus === 'equal') match = true;
                        if (activeRrcFilters.higher && rrcStatus === 'higher') match = true;
                        if (!match) visible = false;
                    }
                }
            }
            row.style.display = visible ? '' : 'none';
        });
        console.log("Filters applied.");
    }

    function processGameData(items, stage) {
        items.forEach(item => {
            if (!item?.id) return;
            if (!gameData[item.id]) gameData[item.id] = {};

            const purchaseOption = item.best_purchase_option || item.purchase_options?.[0];

            if (stage === 'RU' || (stage === 'SINGLE_FETCH' && isRuModeActive)) {
                if (!gameData[item.id].franchises) {
                    gameData[item.id].franchises = item.basic_info?.franchises?.map(f => f.name).join(', ');
                    gameData[item.id].percent_positive = item.reviews?.summary_filtered?.percent_positive;
                    gameData[item.id].review_count = item.reviews?.summary_filtered?.review_count;
                    gameData[item.id].is_early_access = item.is_early_access;
                    gameData[item.id].short_description = item.basic_info?.short_description;
                    gameData[item.id].language_support_russian = item.supported_languages?.find(l => l.elanguage === 8);
                    gameData[item.id].language_support_english = item.supported_languages?.find(l => l.elanguage === 0);
                }
                gameData[item.id].price_ru_initial_cents = getPriceInCents(purchaseOption);
                gameData[item.id].price_ru_formatted_final = purchaseOption?.formatted_final_price;
                if (stage === 'RU') processedRuGames++;
                else processedSingleStageGames++;

            } else if (stage === 'US') {
                gameData[item.id].price_us_initial_cents = getPriceInCents(purchaseOption);
                gameData[item.id].price_us_formatted_final = purchaseOption?.formatted_final_price;
                processedUsGames++;
            } else if (stage === 'SINGLE_FETCH' && !isRuModeActive) {
                if (!gameData[item.id].franchises) {
                    gameData[item.id].franchises = item.basic_info?.franchises?.map(f => f.name).join(', ');
                    gameData[item.id].percent_positive = item.reviews?.summary_filtered?.percent_positive;
                    gameData[item.id].review_count = item.reviews?.summary_filtered?.review_count;
                    gameData[item.id].is_early_access = item.is_early_access;
                    gameData[item.id].short_description = item.basic_info?.short_description;
                    gameData[item.id].language_support_russian = item.supported_languages?.find(l => l.elanguage === 8);
                    gameData[item.id].language_support_english = item.supported_languages?.find(l => l.elanguage === 0);
                }

                processedSingleStageGames++;
            }
        });
        updateProgress();
    }

    async function processRequestQueue() {
        if (isProcessingQueue || !requestQueue.length) {
            if (!isProcessingQueue && isRuModeActive) {
                if (currentProcessingStage === 'RU' && processedRuGames >= totalGamesOnPage) {
                    currentProcessingStage = 'US';
                    updateButtonAndStatus(PROCESS_BUTTON_TEXT.processing_us, STATUS_TEXT.processing_us);
                    console.log("RU processing finished. Starting US processing.");
                    const usBatches = Array.from(collectedAppIds).reduce((acc, id, i) => {
                        if (i % BATCH_SIZE === 0) acc.push([]);
                        acc[acc.length - 1].push(id);
                        return acc;
                    }, []);
                    requestQueue.push(...usBatches.map(batch => ({
                        batch,
                        stage: 'US',
                        lang: 'english',
                        cc: 'US'
                    })));
                    updateProgress();
                    await processRequestQueue();
                } else if (currentProcessingStage === 'US' && processedUsGames >= totalGamesOnPage) {
                    updateButtonAndStatus(PROCESS_BUTTON_TEXT.done, STATUS_TEXT.processing_rrc, true, false);
                    console.log("US processing finished. Calculating and Injecting RRC.");
                    injectAllRrcDisplays();
                    applyAllFilters();
                    isProcessingStarted = false;
                    updateButtonAndStatus(PROCESS_BUTTON_TEXT.done, STATUS_TEXT.done, true, true);
                }
            } else if (!isProcessingQueue && !isRuModeActive && currentProcessingStage === 'SINGLE_FETCH' && processedSingleStageGames >= totalGamesOnPage) {
                console.log("Single stage data fetch complete.");
                applyAllFilters();
                isProcessingStarted = false;
                updateButtonAndStatus(PROCESS_BUTTON_TEXT.done, STATUS_TEXT.done_no_rrc, true, true);
            }
            return;
        }

        isProcessingQueue = true;
        const {
            batch: currentBatch,
            stage: batchStage,
            lang: batchLang,
            cc: batchCC
        } = requestQueue.shift();
        console.log(`Processing ${batchStage} batch of ${currentBatch.length} appids (${batchCC}, ${batchLang})...`);

        try {
            await fetchGameData(currentBatch, batchCC, batchLang, batchStage);
            await new Promise(r => setTimeout(r, REQUEST_DELAY));
        } catch (error) {
            console.error(`Error processing ${batchStage} batch:`, error);
            if (batchStage === 'RU') processedRuGames += currentBatch.length;
            else if (batchStage === 'US') processedUsGames += currentBatch.length;
            else if (batchStage === 'SINGLE_FETCH') processedSingleStageGames += currentBatch.length;
            updateProgress();
        } finally {
            isProcessingQueue = false;
            await processRequestQueue();
        }
    }


    function fetchGameData(appIds, countryCode, language, stage) {
        return new Promise((resolve, reject) => {
            if (!appIds || appIds.length === 0) {
                resolve();
                return;
            }
            const input = {
                ids: appIds.map(appid => ({
                    appid: parseInt(appid, 10)
                })),
                context: {
                    language: language,
                    country_code: countryCode,
                    steam_realm: 1
                },
                data_request: {
                    include_assets: false,
                    include_release: true,
                    include_platforms: false,
                    include_all_purchase_options: true,
                    include_screenshots: false,
                    include_trailers: false,
                    include_ratings: true,
                    include_tag_count: false,
                    include_reviews: true,
                    include_basic_info: true,
                    include_supported_languages: true,
                    include_full_description: false,
                    include_included_items: false
                }
            };
            const url = `${API_URL}?input_json=${encodeURIComponent(JSON.stringify(input))}`;
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                timeout: 15000,
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data?.response?.store_items) {
                                processGameData(data.response.store_items, stage);
                            } else {
                                console.error(`No store_items for ${stage}:`, data);
                                if (stage === 'RU') processedRuGames += appIds.length;
                                else if (stage === 'US') processedUsGames += appIds.length;
                                else if (stage === 'SINGLE_FETCH') processedSingleStageGames += appIds.length;
                            }
                        } catch (e) {
                            console.error(`JSON Error for ${stage}:`, e);
                            if (stage === 'RU') processedRuGames += appIds.length;
                            else if (stage === 'US') processedUsGames += appIds.length;
                            else if (stage === 'SINGLE_FETCH') processedSingleStageGames += appIds.length;
                        }
                    } else {
                        console.error(`API Fail ${stage}: ${response.status}`);
                        if (stage === 'RU') processedRuGames += appIds.length;
                        else if (stage === 'US') processedUsGames += appIds.length;
                        else if (stage === 'SINGLE_FETCH') processedSingleStageGames += appIds.length;
                    }
                    updateProgress();
                    resolve();
                },
                onerror: function(error) {
                    console.error(`Network Error ${stage}:`, error);
                    if (stage === 'RU') processedRuGames += appIds.length;
                    else if (stage === 'US') processedUsGames += appIds.length;
                    else if (stage === 'SINGLE_FETCH') processedSingleStageGames += appIds.length;
                    updateProgress();
                    resolve();
                },
                ontimeout: function() {
                    console.error(`Timeout ${stage} for appids:`, appIds.join(', '));
                    if (stage === 'RU') processedRuGames += appIds.length;
                    else if (stage === 'US') processedUsGames += appIds.length;
                    else if (stage === 'SINGLE_FETCH') processedSingleStageGames += appIds.length;
                    updateProgress();
                    resolve();
                }
            });
        });
    }


    async function startDataCollection() {
        if (isProcessingStarted) {
            console.log("Processing already in progress.");
            return;
        }
        isProcessingStarted = true;
        processedRuGames = 0;
        processedUsGames = 0;
        processedSingleStageGames = 0;
        requestQueue = [];
        gameData = {};

        const rows = document.querySelectorAll('tr.app[data-appid]');
        collectedAppIds = new Set(Array.from(rows).map(r => r.dataset.appid));
        totalGamesOnPage = collectedAppIds.size;

        if (totalGamesOnPage === 0) {
            isProcessingStarted = false;
            updateButtonAndStatus(PROCESS_BUTTON_TEXT.idle, STATUS_TEXT.select_all_slow, false, false);
            return;
        }

        const batches = Array.from(collectedAppIds).reduce((acc, id, i) => {
            if (i % BATCH_SIZE === 0) acc.push([]);
            acc[acc.length - 1].push(id);
            return acc;
        }, []);

        if (isRuModeActive) {
            currentProcessingStage = 'RU';
            updateButtonAndStatus(PROCESS_BUTTON_TEXT.processing_ru, STATUS_TEXT.processing_ru);
            requestQueue = batches.map(batch => ({
                batch,
                stage: 'RU',
                lang: 'russian',
                cc: 'RU'
            }));
        } else {
            currentProcessingStage = 'SINGLE_FETCH';
            updateButtonAndStatus(PROCESS_BUTTON_TEXT.processing_single, STATUS_TEXT.processing_single);


            const currentCC = document.querySelector('details#js-select-cc input[name="cc"]:checked')?.value || document.querySelector('details#js-select-cc')?.dataset.default || 'us';
            requestQueue = batches.map(batch => ({
                batch,
                stage: 'SINGLE_FETCH',
                lang: 'english',
                cc: currentCC
            }));
        }
        updateProgress();
        await processRequestQueue();
    }

    function updateButtonAndStatus(btnText, statusMsg, isDone = false, enableButton = false) {
        const processBtnTextEl = document.getElementById('process-btn-text');
        if (processBtnTextEl) processBtnTextEl.textContent = btnText;
        else if (processButton) processButton.childNodes[1].nodeValue = ` ${btnText}`;


        const statusIndicator = document.querySelector('.status-indicator');
        if (statusIndicator) {
            statusIndicator.textContent = statusMsg;
            statusIndicator.classList.toggle('status-active', isDone);
            statusIndicator.classList.toggle('status-inactive', !isDone && !isProcessingStarted);
        }
        if (processButton) processButton.disabled = !enableButton && isProcessingStarted;
    }


    function updateProgress() {
        const progressBar = document.querySelector('.progress-bar');
        const progressCountEl = document.querySelector('.progress-count');
        const progressPercentEl = document.querySelector('.progress-percent');
        if (!progressBar || !progressCountEl || !progressPercentEl) return;

        let overallPercent = 0;
        let countText = "0/0";

        if (totalGamesOnPage > 0) {
            if (isRuModeActive) {
                if (currentProcessingStage === 'RU') {
                    overallPercent = (processedRuGames / totalGamesOnPage) * 50;
                    countText = `Этап RU: ${processedRuGames}/${totalGamesOnPage}`;
                } else if (currentProcessingStage === 'US') {
                    overallPercent = 50 + (processedUsGames / totalGamesOnPage) * 50;
                    countText = `Этап US: ${processedUsGames}/${totalGamesOnPage}`;
                }
            } else {
                overallPercent = (processedSingleStageGames / totalGamesOnPage) * 100;
                countText = `Обработано: ${processedSingleStageGames}/${totalGamesOnPage}`;
            }
        }
        overallPercent = Math.min(overallPercent, 100);
        progressBar.style.width = `${overallPercent}%`;
        progressCountEl.textContent = countText;
        progressPercentEl.textContent = `(${Math.round(overallPercent)}%)`;


        if (!isProcessingStarted && processButton) {
            const processBtnTextEl = document.getElementById('process-btn-text');
            if (processBtnTextEl) processBtnTextEl.textContent = PROCESS_BUTTON_TEXT.idle;
            else processButton.childNodes[1].nodeValue = ` ${PROCESS_BUTTON_TEXT.idle}`;
            processButton.disabled = false;
        }
    }


    function handleHover(event) {
        const row = event.target.closest('tr.app');
        if (!row || tooltip?.style?.opacity === '1') return;
        clearTimeout(hoverTimer);
        hoverTimer = setTimeout(() => {
            const appId = row.dataset.appid;
            if (gameData[appId] && (gameData[appId].franchises || gameData[appId].language_support_russian)) {
                showTooltip(row, gameData[appId]);
            }
        }, HOVER_DELAY);
        row.addEventListener('mouseleave', hideTooltip, {
            once: true
        });
    }

    function hideTooltip() {
        clearTimeout(hoverTimer);
        if (tooltip) {
            tooltip.style.opacity = '0';
            setTimeout(() => {
                if (tooltip && tooltip.style.opacity === '0') {
                    tooltip.style.display = 'none';
                }
            }, 250);
        }
    }

    function showTooltip(element, data) {
        if (!tooltip) {
            tooltip = document.createElement('div');
            tooltip.className = 'steamdb-tooltip';
            tooltip.addEventListener('mouseenter', () => clearTimeout(hoverTimer));
            tooltip.addEventListener('mouseleave', hideTooltip);
            document.body.appendChild(tooltip);
        }
        tooltip.innerHTML = `
            <div class="tooltip-arrow"></div>
            <div class="tooltip-content">${buildTooltipContent(data)}</div>`;

        const rect = element.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();
        let left = rect.right + window.scrollX + 10;
        let top = rect.top + window.scrollY + (rect.height / 2) - (tooltipRect.height / 2);

        top = Math.max(window.scrollY + 5, top);
        top = Math.min(window.scrollY + window.innerHeight - tooltipRect.height - 5, top);

        const arrow = tooltip.querySelector('.tooltip-arrow');
        if (left + tooltipRect.width > window.scrollX + window.innerWidth - 10) {
            left = rect.left + window.scrollX - tooltipRect.width - 10;
            arrow.style.left = 'auto';
            arrow.style.right = '-10px';
            arrow.style.borderRight = 'none';
            arrow.style.borderLeft = '10px solid #1b2838';
        } else {
            arrow.style.left = '-10px';
            arrow.style.right = 'auto';
            arrow.style.borderLeft = 'none';
            arrow.style.borderRight = '10px solid #1b2838';
        }
        arrow.style.top = `${Math.max(5, Math.min(tooltipRect.height - 15, (element.offsetHeight / 2) - 5))}px`;

        tooltip.style.left = `${left}px`;
        tooltip.style.top = `${top}px`;
        tooltip.style.display = 'block';
        requestAnimationFrame(() => {
            tooltip.style.opacity = '1';
        });
    }

    function buildTooltipContent(data) {
        const reviewClass = getReviewClass(data.percent_positive, data.review_count);
        const earlyAccessClass = data.is_early_access ? 'early-access-yes' : 'early-access-no';
        let languageSupportRussianText = "Отсутствует";
        let languageSupportRussianClass = 'language-no';
        if (data.language_support_russian) {
            let content = [];
            if (data.language_support_russian.supported) content.push("Интерфейс");
            if (data.language_support_russian.subtitles) content.push("Субтитры");
            if (data.language_support_russian.full_audio) content.push("<u>Озвучка</u>");
            languageSupportRussianText = content.join(', ') || "Нет данных";
            languageSupportRussianClass = content.length > 0 ? 'language-yes' : 'language-no';
        }
        let languageSupportEnglishText = "Отсутствует";
        let languageSupportEnglishClass = 'language-no';
        if (data.language_support_english) {
            let content = [];
            if (data.language_support_english.supported) content.push("Интерфейс");
            if (data.language_support_english.subtitles) content.push("Субтитры");
            if (data.language_support_english.full_audio) content.push("<u>Озвучка</u>");
            languageSupportEnglishText = content.join(', ') || "Нет данных";
            languageSupportEnglishClass = content.length > 0 ? 'language-yes' : 'language-no';
        }
        return `
            <div class="group-top"><div class="tooltip-row compact"><strong>Серия игр:</strong> <span class="${!data.franchises ? 'no-data' : ''}">${data.franchises || "Нет данных"}</span></div></div>
            <div class="group-middle">
                <div class="tooltip-row spaced"><strong>Отзывы:</strong> <span class="${reviewClass}">${data.percent_positive !== undefined ? data.percent_positive + '%' : "Нет данных"}</span> (${data.review_count || "0"})</div>
                <div class="tooltip-row spaced"><strong>Ранний доступ:</strong> <span class="${earlyAccessClass}">${data.is_early_access ? "Да" : "Нет"}</span></div>
            </div>
            <div class="group-bottom">
                <div class="tooltip-row language"><strong>Русский язык:</strong> <span class="${languageSupportRussianClass}">${languageSupportRussianText}</span></div>
                ${scriptsConfig.toggleEnglishLangInfo ? `<div class="tooltip-row language"><strong>Английский язык:</strong> <span class="${languageSupportEnglishClass}">${languageSupportEnglishText}</span></div>` : ''}
            </div>
            <div class="tooltip-row description"><strong>Описание:</strong> <span class="${!data.short_description ? 'no-data' : ''}">${data.short_description || "Нет данных"}</span></div>`;
    }

    function getReviewClass(percent, totalReviews) {
        if (totalReviews === undefined || totalReviews === null || totalReviews === 0) return 'no-reviews';
        if (percent === undefined || percent === null) return 'no-reviews';
        if (percent >= 70) return 'positive';
        if (percent >= 40) return 'mixed';
        return 'negative';
    }

    function updateUiForCurrencyMode() {
        const rrcFilterGroup = document.getElementById('rrc-filter-control-group');
        const statusIndicator = document.querySelector('.status-indicator');

        if (rrcFilterGroup) {
            rrcFilterGroup.style.display = isRuModeActive ? '' : 'none';
            if (!isRuModeActive) {
                activeRrcFilters = {
                    lower: false,
                    equal: false,
                    higher: false
                };
                rrcFilterGroup.querySelectorAll('.btn.active').forEach(b => b.classList.remove('active'));
            }
        }
        if (statusIndicator && !isProcessingStarted) {
            statusIndicator.textContent = isRuModeActive ? STATUS_TEXT.select_all_slow : STATUS_TEXT.rrc_disabled;
        }

        if (document.querySelector('tr.app[data-appid]')) {
            applyAllFilters();
            if (!isRuModeActive) {
                document.querySelectorAll('.rrc-display-container').forEach(el => el.remove());
            } else if (isProcessingStarted && currentProcessingStage === '' || !isProcessingStarted && Object.keys(gameData).length > 0) {


            }
        }
    }


    async function init() {
        isRuModeActive = isRuCurrencySelected();
        console.log(`Initializing SteamDB Enhancer. RU Mode Active: ${isRuModeActive}`);

        const style = document.createElement('style');
        style.textContent = styles;
        document.head.append(style);

        const header = document.querySelector('.header-title');
        if (header) {
            const filtersContainer = createFiltersContainer();
            header.parentNode.insertBefore(filtersContainer, header.nextElementSibling);
            processButton = document.getElementById('process-btn');
        } else {
            console.error("Could not find header to insert controls.");
            return;
        }


        document.querySelector('.steamdb-enhancer').addEventListener('click', (e) => {
            handleFilterClick(e);
            handleControlClick(e);
        });


        processButton.addEventListener('click', startDataCollection);
        document.querySelector('#DataTables_Table_0 tbody')?.addEventListener('mouseover', handleHover);


        const currencyDropdown = document.querySelector('details#js-select-cc');
        if (currencyDropdown) {
            const observer = new MutationObserver((mutationsList) => {
                for (let mutation of mutationsList) {
                    if (mutation.type === 'attributes' && mutation.attributeName === 'data-default' ||
                        mutation.target.nodeName === 'INPUT' && mutation.target.type === 'radio' && mutation.target.name === 'cc') {
                        const newRuMode = isRuCurrencySelected();
                        if (newRuMode !== isRuModeActive) {
                            console.log(`Currency changed. New RU Mode Active: ${newRuMode}`);
                            isRuModeActive = newRuMode;

                            const oldFiltersContainer = document.querySelector('.steamdb-enhancer');
                            if (oldFiltersContainer) {
                                const newFiltersContainer = createFiltersContainer();
                                oldFiltersContainer.parentNode.replaceChild(newFiltersContainer, oldFiltersContainer);

                                processButton = document.getElementById('process-btn');
                                processButton.addEventListener('click', startDataCollection);

                                newFiltersContainer.addEventListener('click', (e) => {
                                    handleFilterClick(e);
                                    handleControlClick(e);
                                });
                            }
                            updateUiForCurrencyMode();
                        }
                        break;
                    }
                }
            });
            observer.observe(currencyDropdown, {
                attributes: true,
                childList: true,
                subtree: true
            });
        }


        updateButtonAndStatus(PROCESS_BUTTON_TEXT.idle, isRuModeActive ? STATUS_TEXT.select_all_slow : STATUS_TEXT.rrc_disabled, false, true);
        console.log("SteamDB Enhancer Initialized.");
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();