Steam Custom Price Filter (RU) - v2.3

Добавляет фильтр по минимальной и максимальной цене на страницу поиска Steam. Работает с бесконечной прокруткой и без перезагрузок.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Steam Custom Price Filter (RU) - v2.3
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Добавляет фильтр по минимальной и максимальной цене на страницу поиска Steam. Работает с бесконечной прокруткой и без перезагрузок.
// @author       torch
// @match        https://store.steampowered.com/search/*
// @grant        GM_addStyle
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let debounceTimer;
    let observer; // Объявляем наблюдателя в глобальной области видимости скрипта

    // Добавляем наш CSS-класс для скрытия элементов. !important гарантирует, что наш стиль будет приоритетнее.
    GM_addStyle('.custom-price-hidden { display: none !important; }');

    // --- Функция для создания UI фильтра ---
    function createPriceFilterUI() {
        const rightColumn = document.getElementById('additional_search_options');
        if (!rightColumn) {
            setTimeout(createPriceFilterUI, 500);
            return;
        }
        if (document.getElementById('custom_price_filter_container')) return;

        const filterContainer = document.createElement('div');
        filterContainer.className = 'block search_collapse_block';
        filterContainer.id = 'custom_price_filter_container';
        filterContainer.innerHTML = `
            <div class="block_header">
                <div>Пользовательская цена</div>
            </div>
            <div class="block_content block_content_inner" style="display: block;">
                <div id="custom_price_filter_inputs">
                    <input type="number" id="min_price_input" placeholder="От (руб.)" min="0" step="1">
                    <span>-</span>
                    <input type="number" id="max_price_input" placeholder="До (руб.)" min="0" step="1">
                </div>
            </div>
        `;
        rightColumn.prepend(filterContainer);

        GM_addStyle(`
            #custom_price_filter_inputs { display: flex; align-items: center; justify-content: space-between; gap: 5px; padding: 5px 0; }
            #custom_price_filter_inputs input[type="number"] { width: 42%; padding: 5px; border: 1px solid #3d4450; background-color: #31363f; color: #c7d5e0; border-radius: 3px; }
            #custom_price_filter_inputs input[type="number"]:focus { border-color: #5b9ed8; outline: none; }
            #custom_price_filter_inputs input::-webkit-outer-spin-button,
            #custom_price_filter_inputs input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
            #custom_price_filter_inputs input[type=number] { -moz-appearance: textfield; }
        `);

        // Назначаем обработчики событий
        const minPriceInput = document.getElementById('min_price_input');
        const maxPriceInput = document.getElementById('max_price_input');

        const handleInput = (event) => {
            event.stopPropagation();
            debounce(applyPriceFilter, 300);
        };
        const handleKeydown = (event) => event.stopPropagation();

        minPriceInput.addEventListener('input', handleInput);
        minPriceInput.addEventListener('keydown', handleKeydown);

        maxPriceInput.addEventListener('input', handleInput);
        maxPriceInput.addEventListener('keydown', handleKeydown);
    }

    function debounce(func, delay) {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(func, delay);
    }

    // --- Основная функция фильтрации ---
    function applyPriceFilter() {
        const minPriceInput = document.getElementById('min_price_input');
        const maxPriceInput = document.getElementById('max_price_input');
        if (!minPriceInput || !maxPriceInput) return;

        const minPrice = minPriceInput.value ? parseFloat(minPriceInput.value) * 100 : null;
        const maxPrice = maxPriceInput.value ? parseFloat(maxPriceInput.value) * 100 : null;

        const resultsRows = document.querySelectorAll('#search_resultsRows > a.search_result_row');

        resultsRows.forEach(row => {
            const priceElement = row.querySelector('[data-price-final]');
            const priceAttr = priceElement ? priceElement.getAttribute('data-price-final') : '0';

            let itemPrice = 0;
            if (priceAttr !== null && priceAttr !== "") {
                const parsedPrice = parseInt(priceAttr, 10);
                if (!isNaN(parsedPrice)) itemPrice = parsedPrice;
            }

            const isMinOk = minPrice === null || itemPrice >= minPrice;
            const isMaxOk = maxPrice === null || itemPrice <= maxPrice;

            if (isMinOk && isMaxOk) {
                row.classList.remove('custom-price-hidden');
            } else {
                row.classList.add('custom-price-hidden');
            }
        });
    }

    // --- Функция для запуска наблюдателя ---
    function startObserver() {
        const targetNode = document.getElementById('search_results');
        if (!targetNode) {
            setTimeout(startObserver, 500);
            return;
        }

        observer = new MutationObserver((mutations) => {
            for(const mutation of mutations) {
                // Мы реагируем, если были добавлены новые элементы (игры)
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Используем debounce, чтобы примениться один раз после завершения всех добавлений
                    debounce(applyPriceFilter, 150);
                    break;
                }
            }
        });

        // Наблюдаем за контейнером, в который Steam добавляет новые результаты
        observer.observe(targetNode, { childList: true, subtree: true });
    }

    // --- Инициализация ---
    // Ждем, пока страница полностью загрузится, чтобы все элементы были на месте
    window.addEventListener('load', () => {
        createPriceFilterUI();
        startObserver();
        // Применяем фильтр один раз после загрузки страницы
        setTimeout(applyPriceFilter, 500);
    });

})();