Steam Custom Price Filter (RU) - v2.3

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

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

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

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

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

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

})();