Перейти к содержимому
Zone of Games Forum
0wn3df1x

Скрипты Оунедфикса

Рекомендованные сообщения

Ultimate Steam Enhancer || Tamper Monkey

Описание и ссылки перенесены в блог, посвящённый скрипту.

 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1
  • Спасибо (+1) 2
  • +1 1

Поделиться сообщением


Ссылка на сообщение

SteamDB - Sales; Ultimate Enhancer || Tamper Monkey

Описание и ссылки перенесены в блог, посвящённый скрипту.

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Важная информация для пользователей Google Chrome (версии 138 и новее)

Если вы столкнулись с тем, что скрипты перестали работать (особенно после переустановки/обновления браузера или расширения Tampermonkey), пожалуйста, ознакомьтесь с информацией ниже.

Подробное разъяснение проблемы

Начиная с версии 138, Google внедрила новый, более строгий механизм безопасности для браузера Chrome. Ранее для работы пользовательских скриптов достаточно было включить общий "Режим разработчика" в настройках расширений.

Теперь эта логика изменена. Google считает глобальный переключатель небезопасным и заменила его на индивидуальное разрешение для каждого расширения. Для всех новых установок расширений, способных выполнять скрипты (как Tampermonkey), это разрешение по умолчанию выключено.

Именно поэтому после чистой установки или переустановки Tampermonkey скрипты могут не работать — браузер просто не дает расширению права на выполнение кода.

Пошаговая инструкция по исправлению

Как включить разрешение:

  1. Откройте страницу расширений (три вертикальные точки в правом верхнем углу экрана) > Расширения > Управления расширениями. Самый простой способ — вставить в адресную строку chrome://extensions и нажать Enter.
  2. Найдите в списке установленных расширений Tampermonkey и нажмите на кнопку "Сведения".
  3. На открывшейся странице вы увидите список всех настроек и разрешений для Tampermonkey. Прокрутите вниз и найдите переключатель с названием "Разрешить пользовательские скрипты" (Allow User Scripts).
  4. Активируйте этот переключатель.

Поделиться сообщением


Ссылка на сообщение

SteamDB Sales - Info & Ru Filter || Tamper Monkey — функционал перемещён в SteamDB - Sales; Ultimate Enhancer.

Скрытый текст

 

Описание:

На SteamDB есть очень полезный раздел скидок.
К сожалению, при отсеивании скидок нельзя поставить фильтрацию по наличию русского языка. 
Мой скрипт исправляет эту проблему, добавляя три фильтра:

  • Оставить игры только на русском (предполагает наличие хотя бы текстового перевода).
  • Оставить игры, где есть русская озвучка.
  • Оставить игры без русского языка.

Также он добавляет подсказку справа, при наведении на игру, в которой отображает:

  • Франшизу (серию игр)
  • Фильтрованные отзывы (просеянные алгоритмом Steam, без учёта аномальных).
  • Информацию о том, находится ли игра в раннем доступе
  • Информацию о переводе игры на русский язык (указывает, есть ли перевод интерфейса, озвучки и субтитров).
  • [Информацию об английском языке, если toggleEnglishLangInfo в коде равняется true]
  • Короткое описание игры.

EfYEeQs.png

Скрытый текст
  1. Заходим на страницу Sales на SteamDB.
  2. Проставляем нужную нам валюту и нужные нам фильтры, если необходимо.
    (Я рекомендую ставить количество обзоров >100 и рейтинг больше 70 или 80; учтите, что чем больше игр будет отображаться, тем больше запросов отправится к Steam для получения данных; в одном запросе содержится 100 игр. По внутренним ограничениям SteamDB — система не может вывести больше 10 тысяч игр, т.е. при максимуме игр будет отправлено не больше 100 запросов) 
  3. В выпадающем списке entries per page выбираем All (slow) и ждём, пока все предложения прогрузятся и все запросы отправятся (от 3 до 30 секунд, в зависимости от количества игр).
    yYlYbXX.png
  4. Когда всё прогрузится — при наведении на игры справа появится дополнительная информация о них. 
  5. После прогрузки вы можете жать на кнопки “на русском”, “русская озвучка” и “без русского”. При деактивации кнопки всё возвращается как было.
  6. Если вы хотите, чтобы в подсказке отображалась информация о наличии английского языка, то в настройках проставьте true:
    
    
        const scriptsConfig = {
            toggleEnglishLangInfo: true
        };
Скрытый текст


// ==UserScript==
// @name         SteamDB Sales - Info & Ru Filter
// @namespace    https://steamdb.info/
// @version      1.0
// @description  Показывает дополнительную информацию об в играх в разделе Sales на SteamDB, а также позволяет фильтровать игры по наличию русского языка
// @author       0wn3df1x
// @include     https://steamdb.info/sales/*
// @grant       GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1";
    const BATCH_SIZE = 100;
    const HOVER_DELAY = 300;
    const TOOLTIP_OFFSET = 20;
    const REQUEST_DELAY = 200;

    const scriptsConfig = {
        toggleEnglishLangInfo: false
    };

    let collectedAppIds = new Set();
    let tooltip = null;
    let hoverTimer = null;
    let gameData = {};
    let activeFilter = null;
    let totalGames = 0;
    let processedGames = 0;
    let progressContainer = null;
    let requestQueue = [];
    let isProcessingQueue = false;

    const filterStyles = `
        .filter-container {
            margin: 15px 0;
            padding: 10px;
            border-radius: 3px;
        }
        .filter-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .filter-btn {
            display: flex;
            align-items: center;
            padding: 6px 12px;
            background: #1b2838;
            border-radius: 3px;
            cursor: pointer;
            transition: all 0.2s;
            border: 1px solid #2a3a4d;
        }
        .filter-btn:hover {
            background: #22334d;
        }
        .filter-btn.active {
            background: #66c0f4;
            color: #1b2838;
            border-color: #66c0f4;
        }
        .filter-checkbox {
            display: none;
        }
        .progress-container {
            margin: 15px 0;
            padding: 10px;
            border-radius: 3px;
            width: 50%;
            color: #c6d4df;
        }
        .progress-text {
            font-size: 14px;
            margin-bottom: 5px;
        }
        .progress-bar {
            width: 100%;
            height: 10px;
            background: #2a3a4d;
            border-radius: 5px;
            overflow: hidden;
        }
        .progress-bar-inner {
            height: 100%;
            background: #66c0f4;
            transition: width 0.2s;
        }
    `;

    function createFilters() {
        const container = document.createElement('div');
        container.className = 'filter-container';
        container.innerHTML = `
            <div class="filter-group">
                <div class="filter-btn" data-filter="russian-any">
                    <span>На русском</span>
                </div>
                <div class="filter-btn" data-filter="russian-audio">
                    <span>Русская озвучка</span>
                </div>
                <div class="filter-btn" data-filter="no-russian">
                    <span>Без русского</span>
                </div>
            </div>
        `;

        const header = document.querySelector('.header-title');
        if (header) header.after(container);

        const style = document.createElement('style');
        style.textContent = filterStyles;
        document.head.appendChild(style);
    }

    function createProgressContainer() {
        progressContainer = document.createElement('div');
        progressContainer.className = 'progress-container';
        progressContainer.innerHTML = `
            <div class="progress-text">Обработка игр: <span class="progress-count">0/0</span></div>
            <div class="progress-bar"><div class="progress-bar-inner" style="width: 0%;"></div></div>
        `;

        const filterContainer = document.querySelector('.filter-container');
        if (filterContainer) filterContainer.before(progressContainer);
    }

    function updateProgress() {
        if (!progressContainer) return;

        const progressCount = progressContainer.querySelector('.progress-count');
        const progressBarInner = progressContainer.querySelector('.progress-bar-inner');

        if (progressCount && progressBarInner) {
            progressCount.textContent = `${processedGames}/${totalGames}`;
            const progressPercent = (processedGames / totalGames) * 100;
            progressBarInner.style.width = `${progressPercent}%`;
        }
    }

    function handleFilterClick(event) {
        const btn = event.target.closest('.filter-btn');
        if (!btn) return;

        const filterType = btn.dataset.filter;
        const wasActive = btn.classList.contains('active');

        document.querySelectorAll('.filter-btn').forEach(b => {
            b.classList.remove('active');
        });

        if (!wasActive) {
            btn.classList.add('active');
            activeFilter = filterType;
        } else {
            activeFilter = null;
        }

        applyCurrentFilter();
    }

    function applyCurrentFilter() {
        const rows = document.querySelectorAll('tr.app');
        rows.forEach(row => {
            const appId = row.dataset.appid;
            const data = gameData[appId];
            let visible = true;

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

            row.style.display = visible ? '' : 'none';
        });
    }

    function processGameData(items) {
        items.forEach(item => {
            if (!item || !item.id) {
                console.error('Invalid item data:', item);
                return;
            }

            gameData[item.id] = {
                franchises: item.basic_info?.franchises?.map(f => f.name).join(", "),
                percent_positive: item.reviews?.summary_filtered?.percent_positive,
                review_count: item.reviews?.summary_filtered?.review_count,
                is_early_access: item.is_early_access,
                short_description: item.basic_info?.short_description,
                language_support_russian: item.supported_languages?.find(lang => lang.elanguage === 8),
                language_support_english: item.supported_languages?.find(lang => lang.elanguage === 0)
            };

            processedGames++;
            updateProgress();
        });

        if (processedGames === totalGames) {
            enableFilters();
        }

        if (activeFilter) applyCurrentFilter();
    }

    async function processRequestQueue() {
        if (isProcessingQueue || requestQueue.length === 0) return;

        isProcessingQueue = true;

        while (requestQueue.length > 0) {
            const batch = requestQueue.shift();
            try {
                await fetchGameData(batch);
                await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY));
            } catch (error) {
                console.error('Error processing batch:', error);
            }
        }

        isProcessingQueue = false;
    }

    function fetchGameData(appIds) {
        return new Promise((resolve, reject) => {
            const input = {
                ids: Array.from(appIds).map(appid => ({
                    appid
                })),
                context: {
                    language: "russian",
                    country_code: "US",
                    steam_realm: 1
                },
                data_request: {
                    include_assets: true,
                    include_release: true,
                    include_platforms: true,
                    include_all_purchase_options: true,
                    include_screenshots: true,
                    include_trailers: true,
                    include_ratings: true,
                    include_tag_count: true,
                    include_reviews: true,
                    include_basic_info: true,
                    include_supported_languages: true,
                    include_full_description: true,
                    include_included_items: true,
                    included_item_data_request: {
                        include_assets: true,
                        include_release: true,
                        include_platforms: true,
                        include_all_purchase_options: true,
                        include_screenshots: true,
                        include_trailers: true,
                        include_ratings: true,
                        include_tag_count: true,
                        include_reviews: true,
                        include_basic_info: true,
                        include_supported_languages: true,
                        include_full_description: true,
                        include_included_items: true,
                        include_assets_without_overrides: true,
                        apply_user_filters: false,
                        include_links: true
                    },
                    include_assets_without_overrides: true,
                    apply_user_filters: false,
                    include_links: true
                }
            };

            GM_xmlhttpRequest({
                method: "GET",
                url: `${API_URL}?input_json=${encodeURIComponent(JSON.stringify(input))}`,
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            processGameData(data.response.store_items);
                            resolve();
                        } catch (e) {
                            console.error('Error parsing JSON:', e, response.responseText);
                            processedGames += appIds.length;
                            updateProgress();
                            resolve();
                        }
                    } else {
                        console.error('API request failed:', response.statusText);
                        processedGames += appIds.length;
                        updateProgress();
                        resolve();
                    }
                },
                onerror: function(error) {
                    console.error('API request error:', error);
                    processedGames += appIds.length;
                    updateProgress();
                    resolve();
                }
            });
        });
    }

    function collectAppIds() {
        setTimeout(() => {
            const rows = document.querySelectorAll('tr.app[data-appid]');
            totalGames = rows.length;
            updateProgress();

            const newAppIds = new Set();
            rows.forEach(row => {
                const appId = row.dataset.appid;
                if (!collectedAppIds.has(appId)) {
                    newAppIds.add(appId);
                    collectedAppIds.add(appId);
                }
            });

            if (newAppIds.size > 0) {
                const batches = [];
                const arr = Array.from(newAppIds);
                while (arr.length) batches.push(arr.splice(0, BATCH_SIZE));
                requestQueue.push(...batches);
                processRequestQueue();
            }
        }, 200);
    }

    function enableFilters() {
        const filterButtons = document.querySelectorAll('.filter-btn');
        filterButtons.forEach(btn => {
            btn.disabled = false;
        });
    }

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

        const rect = element.getBoundingClientRect();
        tooltip.style.left = `${rect.right + window.scrollX}px`;
        tooltip.style.top = `${rect.top + window.scrollY - 8}px`;
        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) {
            languageSupportRussianText = "";
            if (data.language_support_russian.supported) languageSupportRussianText += "<br>Интерфейс: ✔ ";
            if (data.language_support_russian.full_audio) languageSupportRussianText += "<br>Озвучка: ✔ ";
            if (data.language_support_russian.subtitles) languageSupportRussianText += "<br>Субтитры: ✔";
            languageSupportRussianClass = languageSupportRussianText ? 'language-yes' : 'language-no';
        }

        let languageSupportEnglishText = "Отсутствует";
        let languageSupportEnglishClass = 'language-no';
        if (data.language_support_english) {
            languageSupportEnglishText = "";
            if (data.language_support_english.supported) languageSupportEnglishText += "<br>Интерфейс: ✔ ";
            if (data.language_support_english.full_audio) languageSupportEnglishText += "<br>Озвучка: ✔ ";
            if (data.language_support_english.subtitles) languageSupportEnglishText += "<br>Субтитры: ✔";
            languageSupportEnglishClass = languageSupportEnglishText ? '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 || "0"}%</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 === 0) return 'no-reviews';
        if (percent >= 70) return 'positive';
        if (percent >= 40) return 'mixed';
        return 'negative';
    }

    function handleHover(event) {
        const row = event.target.closest('tr.app');
        if (!row) return;

        clearTimeout(hoverTimer);

        const appId = row.dataset.appid;
        hoverTimer = setTimeout(() => {
            if (gameData[appId]) {
                showTooltip(row, gameData[appId]);
            }
        }, HOVER_DELAY);

        row.addEventListener('mouseleave', () => {
            clearTimeout(hoverTimer);
            if (tooltip) tooltip.style.opacity = '0';
        }, {
            once: true
        });
    }

    function init() {
        const style = document.createElement('style');
        style.textContent = `
            .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;
            }
            .tooltip-arrow {
                position: absolute;
                left: -10px;
                top: 20px;
                width: 0;
                height: 0;
                border-top: 10px solid transparent;
                border-bottom: 10px solid transparent;
                border-right: 10px solid #1b2838;
            }
            .group-top {
                margin-bottom: 8px;
            }
            .group-middle {
                margin-bottom: 12px;
            }
            .group-bottom {
                margin-bottom: 15px;
            }
            .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; }
        `;

        createFilters();
        createProgressContainer();
        document.querySelector('.filter-container').addEventListener('click', handleFilterClick);
        document.head.appendChild(style);
        document.addEventListener('mouseover', handleHover);

        new MutationObserver(collectAppIds)
            .observe(document.body, {
                childList: true,
                subtree: true
            });

        collectAppIds();
    }

    init();
})();

 


 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

За скрипты спасибо, полезные :beach:

Цитата

Технобог

Факт!

  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

@0wn3df1x а можно создать скрипт по https://store.steampowered.com/account/notinterested/

Скрытый текст

2025-03-06-185528.jpg

у меня там уже свыше 4000 скрытых продуктов, а так как из Steam всякий шлак-игры удаляют, вместо них появляется “Uninitialized”, поэтому я этот список периодически пролистываю и чищу (как-то удалил за раз аж около 40 “Uninitialized”, но каждый раз листать свыше 100 страниц муторно да и можно пропустить.
Так вот, можно как-то с помощью скрипта сделать, чтобы на этой странице отобразить только продукты “Uninitialized”?

 

Поделиться сообщением


Ссылка на сообщение
1 час назад, PermResident сказал:

@0wn3df1x а можно создать скрипт по https://store.steampowered.com/account/notinterested/

  Скрыть содержимое

2025-03-06-185528.jpg

у меня там уже свыше 4000 скрытых продуктов, а так как из Steam всякий шлак-игры удаляют, вместо них появляется “Uninitialized”, поэтому я этот список периодически пролистываю и чищу (как-то удалил за раз аж около 40 “Uninitialized”, но каждый раз листать свыше 100 страниц муторно да и можно пропустить.
Так вот, можно как-то с помощью скрипта сделать, чтобы на этой странице отобразить только продукты “Uninitialized”?

 

  • Сперва жмём кнопку “Собрать список скрытых игр”

aoyCRUQ.png

  • Затем жмём кнопку “Узнать недоступные игры” и ждём

tbOl5AG.png

  • Затем жмём “Убрать недоступные игры” и ждём.

UZKyDBz.png


Удаление происходит долго. Между удалениями пауза в 3 секунды (иначе стим быстро накидывает блок; если блок всё же накинут — скрипт должен возобновить работу через 3 минуты), поэтому на удаление 100 игр понадобится 300 секунд или 5 минут. 1000 игр — 50 минут. И так далее.

Скрытый текст

// ==UserScript==
// @name         Steam Ignored Apps Cleaner
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Удаление недоступных игр из списка скрытых в Steam
// @author       0wn3df1x
// @match        https://store.steampowered.com/account/notinterested/*
// @icon         https://store.steampowered.com/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      store.steampowered.com
// @connect      api.steampowered.com
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function() {
    'use strict';

    let state = {
        ignoredApps: [],
        unavailableApps: [],
        currentStep: 0,
        totalSteps: 0,
        isBlocked: false
    };

    // Стили для интерфейса
    GM_addStyle(`
        .cleaner-container {
            background: #1b2838;
            padding: 20px;
            margin: 20px 0;
            border-radius: 3px;
            color: #c6d4df;
        }
        .cleaner-section {
            margin-bottom: 15px;
        }
        .cleaner-button {
            background: linear-gradient(to bottom, #799905 5%, #536904 95%);
            border: 1px solid #000;
            padding: 5px 10px;
            color: #fff;
            cursor: pointer;
            border-radius: 2px;
            margin-right: 10px;
        }
        .cleaner-button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        .progress-container {
            width: 100%;
            background: #000;
            height: 20px;
            margin: 10px 0;
        }
        .progress-bar {
            height: 100%;
            background: #67c1f5;
            transition: width 0.3s;
        }
        .status-message {
            color: #67c1f5;
            margin: 5px 0;
        }
        .error-message {
            color: #ff4747;
        }
    `);

    // Создание интерфейса
    function createUI() {
        const container = $(`
            <div class="cleaner-container">
                <h2>Очистка скрытых игр</h2>
                <div class="cleaner-section">
                    <button id="collectButton" class="cleaner-button">Собрать список скрытых игр</button>
                    <span id="ignoredCount"></span>
                </div>
                <div class="cleaner-section">
                    <button id="checkButton" class="cleaner-button" disabled>Узнать недоступные игры</button>
                    <span id="unavailableCount"></span>
                </div>
                <div class="cleaner-section">
                    <button id="removeButton" class="cleaner-button" disabled>Убрать недоступные игры</button>
                    <div class="progress-container">
                        <div class="progress-bar" style="width: 0%"></div>
                    </div>
                    <div class="status-message"></div>
                </div>
            </div>
        `).insertAfter('.pageheader');

        return container;
    }

    // Логирование и отображение статуса
    function updateStatus(message, isError = false) {
        const status = $('.status-message');
        status.text(message).toggleClass('error-message', isError);
        console.log(isError ? '❌ ' + message : 'ℹ️ ' + message);
    }

    // Получение данных пользователя
    async function fetchUserData() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://store.steampowered.com/dynamicstore/userdata/',
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        resolve(data);
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: reject
            });
        });
    }

    // Проверка доступности игр
    async function checkAvailability(appIds) {
        const BATCH_SIZE = 150;
        const MAX_PARALLEL = 5;
        let batches = [];
        let results = [];

        // Формирование батчей
        for (let i = 0; i < appIds.length; i += BATCH_SIZE) {
            batches.push(appIds.slice(i, i + BATCH_SIZE));
        }

        updateStatus(`Начинаем проверку ${appIds.length} игр...`);

        for (let i = 0; i < batches.length; i += MAX_PARALLEL) {
            const currentBatches = batches.slice(i, i + MAX_PARALLEL);
            const requests = currentBatches.map(batch => {
                const ids = batch.map(id => ({appid: id}));
                const url = `https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json=${
                    encodeURIComponent(JSON.stringify({
                        ids: ids,
                        context: {language: "english", country_code: "US", steam_realm: 1},
                        data_request: {include_basic_info: true}
                    }))
                }`;

                return new Promise(resolve => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        onload: function(response) {
                            try {
                                const data = JSON.parse(response.responseText);
                                resolve(data.response.store_items);
                            } catch (e) {
                                resolve([]);
                            }
                        },
                        onerror: () => resolve([])
                    });
                });
            });

            const responses = await Promise.all(requests);
            results.push(...responses.flat());
            updateStatus(`Проверено ${Math.min((i + MAX_PARALLEL) * BATCH_SIZE, appIds.length)} из ${appIds.length} игр`);
        }

        return results.filter(item => item.success === 15 && !item.visible).map(item => item.appid);
    }

    // Основной процесс
    $(async function() {
        const ui = createUI();

        // Сбор игр
        $('#collectButton').click(async function() {
            try {
                updateStatus('Получаем список скрытых игр...');
                const userData = await fetchUserData();
                state.ignoredApps = Object.keys(userData.rgIgnoredApps).map(Number);
                $('#ignoredCount').text(`Скрыто ${state.ignoredApps.length} игр`);
                $('#checkButton').prop('disabled', false);
                updateStatus('Список скрытых игр получен!');
            } catch (e) {
                updateStatus('Ошибка при получении списка игр', true);
            }
        });

        // Проверка доступности
        $('#checkButton').click(async function() {
            try {
                $('#checkButton').prop('disabled', true);
                state.unavailableApps = await checkAvailability(state.ignoredApps);
                $('#unavailableCount').text(`Недоступно ${state.unavailableApps.length} игр`);
                $('#removeButton').prop('disabled', false);
                updateStatus('Проверка завершена!');
            } catch (e) {
                updateStatus('Ошибка при проверке доступности', true);
            }
        });

        // Удаление игр
        $('#removeButton').click(async function() {
            const total = state.unavailableApps.length;
            let processed = 0;
            let retries = 0;

            async function processApp(appId) {
                return new Promise((resolve, reject) => {
                    setTimeout(async () => {
                        try {
                            const result = await $J.post('https://store.steampowered.com/recommended/ignorerecommendation/', {
                                sessionid: g_sessionID,
                                appid: appId,
                                snr: '1_account_notinterested_',
                                remove: 1
                            });

                            $J('#ignored_app_' + appId).slideUp();
                            GDynamicStore.InvalidateCache();
                            processed++;
                            $('.progress-bar').css('width', `${(processed / total) * 100}%`);
                            updateStatus(`Удалено ${processed} из ${total}`);
                            resolve();
                        } catch (e) {
                            if (e.status === 429) {
                                updateStatus('Слишком много запросов! Ждем 3 минуты...', true);
                                await new Promise(r => setTimeout(r, 180000));
                                retries++;
                                if (retries < 3) await processApp(appId);
                                else reject();
                            } else {
                                reject();
                            }
                        }
                    }, 3000);
                });
            }

            try {
                $('#removeButton').prop('disabled', true);
                for (const appId of state.unavailableApps) {
                    await processApp(appId);
                }
                updateStatus('Все игры успешно удалены!');
            } catch (e) {
                updateStatus('Произошла ошибка при удалении игр', true);
            }
        });
    });
})();

 

  • Спасибо (+1) 1

Поделиться сообщением


Ссылка на сообщение

@0wn3df1x благодарю :good:Буду пробовать.

Поделиться сообщением


Ссылка на сообщение

Рулетка для Stelicas || HTML

Stelicas (Steam Library Categories Scraper) - инструмент для извлечения категорий вашей библиотеки Steam, получения подробной информации об играх (включая теги, даты выпуска, отзывы и многое другое) и экспорта данных в структурированный CSV-формат для удобной организации и анализа.

Ссылка на пост с программой

Зачем нужна рулетка?
Сейчас есть сайты, на которых есть рулетка по играм с пользовательских аккаунтов в Steam, но эти сайты позволяют крутить только по всей библиотеке. Данная рулетка позволяет крутить по конкретным категориям (коллекциями) пользователей. Допустим, если у вас 50 игр в избранном или в категории "Horror" или “Купил пьяным” - можно крутить только их. 

Скрытый текст

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Stelicas Roulette</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: #1b2838;
            color: #c6d4df;
            margin: 0;
            padding: 20px;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        .file-section {
            margin-bottom: 20px;
        }

        .roulette-section {
            display: flex;
            gap: 20px;
        }

        .roulette-container {
            flex-grow: 1;
            position: relative;
            overflow: hidden;
            height: 200px;
            border: 2px solid #67c1f5;
            border-radius: 8px;
        }

        #roulette {
            display: flex;
            height: 100%;
            position: relative;
            will-change: transform;
        }

        .game-item {
            min-width: 200px;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            border-right: 1px solid #67c1f533;
            padding: 10px;
            box-sizing: border-box;
        }

        .game-item img {
            max-width: 100%;
            max-height: 100%;
        }

        .selector {
            position: absolute;
            left: 50%;
            top: 0;
            height: 100%;
            width: 4px;
            background: #ff0000;
            transform: translateX(-50%);
            animation: pulse 1.5s infinite;
        }
        @keyframes pulse {
            0% { opacity: 0.8; }
            50% { opacity: 0.4; }
            100% { opacity: 0.8; }
        }

    .result {
        margin-top: 20px;
        background: #1b2838;
        border-radius: 4px;
        padding: 20px;
        display: none;
        border: 1px solid #67c1f533;
    }

    .result-header {
        display: flex;
        align-items: center;
        gap: 15px;
        margin-bottom: 20px;
    }

    .game-poster {
        width: 100%;
        max-width: 460px;
        border-radius: 4px;
        overflow: hidden;
    }

    .game-poster img {
        width: 100%;
        height: auto;
        display: block;
    }

    .game-title {
        font-size: 26px;
        color: #fff;
        margin: 0;
    }

    .game-content {
        display: grid;
        grid-template-columns: 2fr 1fr;
        gap: 30px;
        margin-top: 20px;
    }

    .game-details {
        background: #171a21;
        padding: 20px;
        border-radius: 4px;
    }

    .detail-item {
        margin-bottom: 15px;
        padding-bottom: 15px;
        border-bottom: 1px solid #67c1f533;
    }

    .detail-item:last-child {
        border-bottom: 0;
        margin-bottom: 0;
        padding-bottom: 0;
    }

    .detail-label {
        color: #67c1f5;
        font-size: 12px;
        margin-bottom: 5px;
    }

    .detail-value {
        color: #c6d4df;
        font-size: 14px;
    }

    .game-description {
        line-height: 1.5;
        font-size: 14px;
        color: #8f98a0;
    }

    .steam-link {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        color: #67c1f5;
        text-decoration: none;
        font-size: 14px;
        margin-top: 15px;
    }

    .steam-link:hover {
        color: #fff;
    }

    .game-tags {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 15px;
    }

    .tag {
        background: #67c1f510;
        color: #67c1f5;
        padding: 4px 10px;
        border-radius: 2px;
        font-size: 12px;
    }

    .launch-button {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        background: #5ba32b;
        color: white;
        padding: 12px 25px;
        border-radius: 4px;
        text-decoration: none;
        margin-top: 20px;
        transition: background 0.2s;
        font-weight: bold;
    }

    .launch-button:hover {
        background: #48961a;
    }

    .review-rating {
        display: inline-flex;
        align-items: center;
        gap: 5px;
        background: #1a3b5a;
        color: #2B80E9;
        padding: 4px 8px;
        border-radius: 2px;
        font-size: 14px;
    }

    .icon {
        width: 16px;
        height: 16px;
    }

        select, button {
            background: #67c1f5;
            color: #1b2838;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            margin: 5px;
        }

        button:hover {
            background: #4f9fd1;
        }


        .launch-button {
            display: inline-block;
            background: #5ba32b;
            color: white;
            padding: 10px 20px;
            border-radius: 4px;
            text-decoration: none;
            margin-top: 10px;
            transition: background 0.2s;
        }
        .launch-button:hover {
            background: #48961a;
        }

    .review-rating.positive { 
        background: #366c22; 
        color: #a4d007; 
    }
    .review-rating.mixed { 
        background: #4d3d00; 
        color: #FFD700; 
    }
    .review-rating.negative { 
        background: #5a1a1a; 
        color: #E53E3E; 
    }

.priority-checkbox {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 10px 0;
    color: #c6d4df;
}

.priority-checkbox input {
    margin: 0;
    width: 16px;
    height: 16px;
}
    </style>
</head>
<body>
    <div class="container">
        <div class="file-section">
            <input type="file" id="csvFile" accept=".csv">
            <button onclick="loadCSV()">Подгрузить</button>
        </div>

        <div>
            <select id="categorySelect"></select>
            <button onclick="applyCategory()">Подтвердить категорию</button>
        </div>

        <div class="roulette-section">
            <div class="roulette-container">
                <div id="roulette"></div>
                <div class="selector"></div>
            </div>
        </div>

        <button onclick="spin()">Крутить!</button>

<label class="priority-checkbox">
    <input type="checkbox" id="priorityCheckbox">
    Включить приоритеты (учет отзывов)
</label>

<div class="result" id="result">
    <div class="result-header">
        <div class="game-poster">
            <img id="result-poster" src="" alt="Постер игры" onerror="this.style.display='none'">
        </div>
        <div>
            <h1 class="game-title" id="result-title"></h1>
            <div class="review-rating" id="result-rating"></div>
            <a href="#" class="steam-link" target="_blank" id="result-steam-link">
                <svg class="icon" viewBox="0 0 496 512" width="16" height="16">
                    <path fill="currentColor" d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 248.4 8 385.7 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"/>
                </svg>
                Страница в Steam
            </a>
        </div>
    </div>
    
    <div class="game-content">
        <div>
            <p class="game-description" id="result-description"></p>
            <div class="game-tags" id="result-tags"></div>
            <a href="#" class="launch-button" id="result-launch-link">
                <svg class="icon" viewBox="0 0 448 512" width="16" height="16">
                    <path fill="currentColor" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/>
                </svg>
                Запустить игру
            </a>
        </div>
        
        <div class="game-details">
            <div class="detail-item">
                <div class="detail-label">Дата выхода</div>
                <div class="detail-value" id="result-release-date"></div>
            </div>
            <div class="detail-item">
                <div class="detail-label">Издатель</div>
                <div class="detail-value" id="result-publisher"></div>
            </div>
            <div class="detail-item">
                <div class="detail-label">Разработчик</div>
                <div class="detail-value" id="result-developer"></div>
            </div>
            <div class="detail-item">
                <div class="detail-label">Ваш язык</div>
                <div class="detail-value" id="result-languages"></div>
            </div>
        </div>
    </div>
</div>

    <script>
        let games = [];
        let currentGames = [];
        let categories = new Map();
        let spinning = false;
        const CLONES_COUNT = 5; // Количество клонов для плавной анимации

        function loadCSV() {
            const file = document.getElementById('csvFile').files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(e) {
                parseCSV(e.target.result);
                updateCategorySelect();
            };
            reader.readAsText(file);
        }

        function parseCSV(data) {
            const rows = data.split('\n').slice(1);
            games = [];
            categories.clear();

            rows.forEach(row => {
                if (!row.trim()) return;
                const fields = row.split('\t');
                if (fields.length < 16) return;

const game = {
    game_id: fields[0],
    name: fields[1],
    categories: fields[2].split(';'),
    type: fields[3],
    tags: fields[4],
    release_date: fields[5],
    review_percentage: parseFloat(fields[6]) || 0,
    review_count: parseInt(fields[7]) || 0,
    is_free: fields[8],
    is_early_access: fields[9],
    publishers: fields[10],
    developers: fields[11],
    franchises: fields[12],
    short_description: fields[13],
    supported_language: fields[14],
    'Steam-Link': fields[15],
    Pic: fields[16]?.trim()
};

                games.push(game);
                game.categories.forEach(cat => {
                    categories.set(cat, (categories.get(cat) || 0) + 1);
                });
            });

            categories = new Map([['Всё', games.length], ...categories]);
        }

function updateCategorySelect() {
    const select = document.getElementById('categorySelect');
    select.innerHTML = '';
    
    // Создаем массив из категорий для сортировки
    const sortedCategories = Array.from(categories.entries()).sort((a, b) => {
        const [catA] = a;
        const [catB] = b;
        
        // "Всё" всегда на первом месте
        if (catA === 'Всё') return -1;
        if (catB === 'Всё') return 1;
        
        // "Избранное" всегда на втором месте
        if (catA === 'Избранное') return -1;
        if (catB === 'Избранное') return 1;
        
        // Остальные сортируем по алфавиту
        return catA.localeCompare(catB);
    });

    // Добавляем опции в выпадающий список
    sortedCategories.forEach(([cat, count]) => {
        const option = document.createElement('option');
        option.value = cat;
        option.textContent = `${cat} (${count})`;
        select.appendChild(option);
    });
}

        function applyCategory() {
            const selected = document.getElementById('categorySelect').value;
            currentGames = selected === 'Всё' ? games : 
                games.filter(game => game.categories.includes(selected));
            
            updateRoulette();
        }



function spin() {
    if (spinning || !currentGames.length) return;
    
    spinning = true;
    const roulette = document.getElementById('roulette');
    const itemWidth = 200;
    const originalCount = currentGames.length;
    const totalItems = originalCount * CLONES_COUNT;
    
    // Выбор целевой игры
    let targetIndex;
    const usePriorities = document.getElementById('priorityCheckbox').checked;
    
    if (usePriorities) {
        // Рассчитываем веса для игр
        const weights = currentGames.map(game => {
            const percent = parseFloat(game.review_percentage) || 0;
            const count = parseInt(game.review_count) || 0;
            return percent * count;
        });
        
        // Нормализуем веса и создаем распределение
        const totalWeight = weights.reduce((sum, w) => sum + w, 0);
        const normalized = weights.map(w => w / totalWeight);
        
        // Выбираем индекс с учетом весов
        const random = Math.random();
        let cumulative = 0;
        for (let i = 0; i < normalized.length; i++) {
            cumulative += normalized[i];
            if (random <= cumulative) {
                targetIndex = i;
                break;
            }
        }
        targetIndex = targetIndex || 0; // На случай ошибок округления
    } else {
        // Случайный выбор без приоритетов
        targetIndex = Math.floor(Math.random() * originalCount);
    }
    
    // Рассчитываем позицию с учетом клонов (ставим в середину третьего клона)
    const targetPosition = (targetIndex + originalCount * 2) * itemWidth 
        - roulette.parentElement.offsetWidth/2 
        + itemWidth/2;
    
    const startPosition = parseFloat(roulette.style.transform?.replace('translateX(-', '').replace('px)', '')) || 0;
    const startTime = performance.now();
    const duration = 3000;
    
    function animate(timestamp) {
        const elapsed = timestamp - startTime;
        const progress = Math.min(elapsed / duration, 1);
        const easedProgress = 1 - Math.pow(1 - progress, 3);
        
        const currentPosition = startPosition + (targetPosition - startPosition) * easedProgress;
        roulette.style.transform = `translateX(-${currentPosition}px)`;
        
        if(progress < 1) {
            requestAnimationFrame(animate);
        } else {
            spinning = false;
            showResult(currentGames[targetIndex]);
        }
    }
    
    requestAnimationFrame(animate);
}

        function updateRoulette() {
            const roulette = document.getElementById('roulette');
            roulette.innerHTML = '';
            
            if(currentGames.length === 0) return;

            // Создаем клонированный набор игр
            const clonedGames = [];
            for(let i = 0; i < CLONES_COUNT; i++) {
                clonedGames.push(...currentGames);
            }

            clonedGames.forEach(game => {
                const div = document.createElement('div');
                div.className = 'game-item';
                
                if (game.Pic) {
                    const img = document.createElement('img');
                    img.src = game.Pic;
                    img.alt = game.name;
                    div.appendChild(img);
                } else {
                    div.textContent = game.name || game.game_id;
                }
                
                roulette.appendChild(div);
            });

            // Сбрасываем позицию рулетки
            roulette.style.transform = 'translateX(0)';
        }

function showResult(game) {
    const result = document.getElementById('result');
    const steamLink = `https://store.steampowered.com/app/${game.game_id}/`;
    const launchLink = `steam://launch/${game.game_id}/Dialog`;

    // Постер
    const posterImg = document.getElementById('result-poster');
    posterImg.src = game.Pic || '';
    posterImg.style.display = game.Pic ? 'block' : 'none';
    
    // Основная информация
    document.getElementById('result-title').textContent = game.name || 'Без названия';
    document.getElementById('result-steam-link').href = steamLink;
    document.getElementById('result-launch-link').href = launchLink;
    
    // Рейтинг
    const ratingElem = document.getElementById('result-rating');
    let ratingClass = '';
    if(game.review_percentage) {
        const percent = parseInt(game.review_percentage);
        if(percent >= 70) ratingClass = 'positive';
        else if(percent >= 40) ratingClass = 'mixed';
        else ratingClass = 'negative';
        
        ratingElem.innerHTML = `
            <span>${percent}%</span>
            <span>(${game.review_count || 0} отзывов)</span>
        `;
        ratingElem.className = `review-rating ${ratingClass}`;
    } else {
        ratingElem.textContent = 'Нет отзывов';
        ratingElem.className = 'review-rating';
    }

    // Описание
    document.getElementById('result-description').textContent = 
        game.short_description || 'Описание отсутствует';
    
    // Теги
    const tagsContainer = document.getElementById('result-tags');
    tagsContainer.innerHTML = '';
    if(game.tags) {
        game.tags.split(';').forEach(tag => {
            if(tag.trim()) {
                const span = document.createElement('span');
                span.className = 'tag';
                span.textContent = tag.trim();
                tagsContainer.appendChild(span);
            }
        });
    }

    // Детали
    document.getElementById('result-release-date').textContent = 
        game.release_date || 'Неизвестно';
    document.getElementById('result-publisher').textContent = 
        game.publishers || 'Не указан';
    document.getElementById('result-developer').textContent = 
        game.developers || 'Не указан';
    
    // Обработка языков
    document.getElementById('result-languages').textContent = 
        parseSupportedLanguages(game.supported_language);

    // Показываем блок
    result.style.display = 'block';
}

function parseSupportedLanguages(langStr) {
    if (!langStr || langStr.trim() === '') return 'Не указаны';
    
    // Убираем фигурные скобки
    const cleaned = langStr.replace(/[{}]/g, '');
    
    // Обрабатываем специальные случаи
    if (cleaned === 'TRUE') return 'Полная локализация';
    if (cleaned === 'FALSE') return 'Локализация отсутствует';
    
    // Разбиваем на части
    const parts = cleaned.split(';').map(p => p.trim().toLowerCase());
    
    // Проверяем правильный формат (должно быть 3 элемента)
    if (parts.length !== 3) return 'Не указаны';
    
    // Категории локализации
    const categories = ['Интерфейс', 'Озвучка', 'Субтитры'];
    const activeCategories = [];
    
    parts.forEach((part, index) => {
        if (part === 'true') {
            activeCategories.push(categories[index]);
        }
    });
    
    if (activeCategories.length === 0) return 'Локализация отсутствует';
    return activeCategories.join('; ');
}
    </script>
</body>
</html>

 

Как использовать:

  1. Соберите данные по категориям в аккаунте с помощью Stelicas.
  2. Создайте текстовый файл и вставьте в него код из поста.
  3. Поменяйте расширение у созданного файла на html — с него сможете запускать рулетку.
  4. Откройте html с рулеткой в любом браузере.
  5. Загрузите файл final_data.csv, лежащий по пути Stelicas\output\final_data.csv  
  6. Выбираете категорию вашей библиотеки из выпадающего списка и нажимаете подгрузить
  7. Можете крутить - выпадет случайная игра.
    • Если проставите галочку на приоритете, то у игр с высоким рейтингом и большим количеством отзывов будет больше шансов выпасть.

67wLCzy.png

Изменено пользователем 0wn3df1x

Поделиться сообщением


Ссылка на сообщение

Plati.Market MegaSearch || Tamper Monkey — функционал перемещён в Plati.Market; Ultimate Enhancer.

Скрытый текст

// ==UserScript==
// @name         Plati.Market MegaSearch
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Добавляет расширенный поиск с сортировкой и фильтрацией на Plati.Market
// @author       0wn3df1x
// @match        https://plati.market/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=plati.market
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.digiseller.com
// @connect      plati.market
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Конфигурация ---
    const API_BASE_URL = 'https://api.digiseller.com/api/products/search2';
    const SUGGEST_API_URL = 'https://plati.market/api/suggest.ashx';
    const IMAGE_DOMAIN = 'digiseller.mycdn.ink';
    const RESULTS_PER_PAGE_CHECK = 1;
    const DEFAULT_SORT_MODE = 2;
    const SUGGEST_DEBOUNCE_MS = 300;

    // --- Глобальные переменные ---
    let currentResults = [];
    let currentSort = { field: 'relevance', direction: 'desc' };
    let firstSortClick = { price: true, sales: true, name: true, relevance: false };
    let exclusionKeywords = GM_getValue('megaSearchExclusions', []);
    let suggestDebounceTimeout;

    // --- Стили ---
    GM_addStyle(`
        #megaSearchModal {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(20, 20, 25, 0.97);
            z-index: 9999; display: none; color: #eee; overflow-y: auto;
            font-family: "Inter", sans-serif;
        }
        #megaSearchModal * { box-sizing: border-box; }
        #megaSearchContainer {
            max-width: 1300px; margin: 20px auto; padding: 20px; position: relative;
        }
        #megaSearchCloseBtn {
            position: absolute; top: 15px; right: 20px; font-size: 35px; color: #aaa;
            background: none; border: none; cursor: pointer; line-height: 1; z-index: 10;
        }
        #megaSearchCloseBtn:hover { color: #fff; }
        #megaSearchHeader { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; flex-wrap: wrap; position: relative; z-index: 5; }
        .megaSearchInputContainer { position: relative; flex-grow: 1; min-width: 200px; }
        #megaSearchInput {
            width: 100%; padding: 10px 15px; font-size: 16px; background-color: #333;
            border: 1px solid #555; color: #eee; border-radius: 4px; height: 38px;
        }
        #megaSearchSuggestions {
             position: absolute; top: 100%; left: 0; right: 0; background-color: #3a3a40;
             border: 1px solid #555; border-top: none; border-radius: 0 0 4px 4px;
             max-height: 300px; overflow-y: auto; z-index: 10000; display: none;
         }
        .suggestionItem { padding: 8px 15px; cursor: pointer; color: #eee; font-size: 14px; border-bottom: 1px solid #4a4a50; }
        .suggestionItem:last-child { border-bottom: none; }
        .suggestionItem:hover { background-color: #4a4a55; }
        .megaSearchInputGroup {
            display: inline-flex; align-items: stretch; margin-left: 15px;
            border: 1px solid #555; border-radius: 4px;
            background-color: #333; overflow: hidden; height: 38px;
        }
        #megaSearchExcludeInput { padding: 8px 12px; font-size: 14px; background-color: transparent; border: none; color: #eee; outline: none; border-radius: 0; }
        #megaSearchAddExcludeBtn { display: flex; align-items: center; justify-content: center; padding: 0 10px; background-color: #555; border: none; cursor: pointer; border-radius: 0; color: #eee; }
        #megaSearchAddExcludeBtn:hover { background-color: #666; }
        #megaSearchAddExcludeBtn svg { width: 16px; height: 16px; fill: currentColor; }
        .megaSearchBtn {
            padding: 10px 15px; font-size: 14px; color: white; border: none;
            border-radius: 4px; cursor: pointer; white-space: nowrap; height: 38px;
            display: inline-flex; align-items: center; justify-content: center;
        }
        #megaSearchGoBtn { background-color: #4D88FF; }
        #megaSearchGoBtn:hover { background-color: #3366CC; }
        .megaSearchBtn.sortBtn { background-color: #555; }
        .megaSearchBtn.sortBtn.active { background-color: #007bff; }
        .megaSearchBtn.sortBtn:hover { background-color: #666; }
        .megaSearchBtn.sortBtn.active:hover { background-color: #0056b3; }
        #megaSearchResults { display: flex; flex-wrap: wrap; gap: 15px; justify-content: flex-start; margin-top: 20px; }
        #megaSearchResultsStatus { width: 100%; text-align: center; font-size: 18px; color: #aaa; padding: 50px 0; }
        #megaSearchExclusionTags {
            position: fixed; top: 180px; right: 20px; width: 250px;
            max-height: calc(100vh - 220px); overflow-y: auto; z-index: 5;
            display: flex; flex-direction: row; flex-wrap: wrap;
            justify-content: flex-end; align-items: flex-start; gap: 8px; padding: 5px;
            scrollbar-width: thin; scrollbar-color: #555 #333;
        }
        #megaSearchExclusionTags::-webkit-scrollbar { width: 5px; }
        #megaSearchExclusionTags::-webkit-scrollbar-track { background: #333; border-radius: 3px;}
        #megaSearchExclusionTags::-webkit-scrollbar-thumb { background-color: #555; border-radius: 3px; }
        .exclusionTag {
            display: inline-block; background-color: rgba(70, 70, 80, 0.8); color: #ddd;
            padding: 5px 10px; border-radius: 15px; font-size: 13px; cursor: pointer;
            transition: background-color 0.2s; border: 1px solid rgba(100, 100, 110, 0.8);
            white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
        }
        .exclusionTag:hover { background-color: rgba(220, 53, 69, 0.8); border-color: rgba(200, 40, 50, 0.9); color: #fff; }
        .exclusionTag::after { content: ' ×'; font-weight: bold; margin-left: 4px; }
        .megaSearchItem {
            background-color: #2a2a30; border-radius: 8px; padding: 10px;
            width: calc(16.66% - 13px); min-width: 160px; display: flex; flex-direction: column;
            transition: transform 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            position: relative; color: #ccc; font-size: 13px;
        }
         .megaSearchItem:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
         .megaSearchItem a { text-decoration: none; color: inherit; display: flex; flex-direction: column; height: 100%; }
         .megaSearchItem img { width: 100%; height: auto; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 6px; margin-bottom: 8px; background-color: #444; }
         .megaSearchItem .price { font-size: 16px; font-weight: 700; color: #6cff5c; margin-bottom: 5px; }
         .megaSearchItem .title { font-size: 13px; font-weight: 500; line-height: 1.3; height: 3.9em; overflow: hidden; text-overflow: ellipsis; margin-bottom: 8px; color: #eee; flex-grow: 1; }
         .megaSearchItem .sales, .megaSearchItem .seller { font-size: 11px; color: #aaa; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
         .megaSearchItem .buyButton { display: block; text-align: center; padding: 8px; margin-top: 10px; background-color: #007bff; color: white; border-radius: 4px; font-size: 13px; font-weight: 600; }
         .megaSearchItem .buyButton:hover { background-color: #0056b3; }
        /* Адаптивность */
        @media (max-width: 1400px) { .megaSearchItem { width: calc(20% - 12px); } }
        @media (max-width: 1199px) { .megaSearchItem { width: calc(25% - 12px); } }
        @media (max-width: 991px) { .megaSearchItem { width: calc(33.33% - 10px); } }
        @media (max-width: 767px) {
             .megaSearchItem { width: calc(50% - 8px); }
             #megaSearchHeader { gap: 10px; }
             .megaSearchBtn { padding: 8px 12px; font-size: 13px; height: 36px; }
             .megaSearchInputGroup { margin-left: 5px; height: 36px;}
             #megaSearchExcludeInput { font-size: 13px; padding: 8px 10px; height: 34px; }
             #megaSearchAddExcludeBtn { height: 34px; }
             #megaSearchExclusionTags { display: none; }
             #megaSearchSuggestions { font-size: 14px; }
             .suggestionItem { padding: 6px 10px; }
        }
        @media (max-width: 575px) { .megaSearchItem { width: calc(100% - 0px); } #megaSearchInput { font-size: 14px; } }
        .hidden-by-filter { display: none !important; }
         #megaSearchLaunchBtn { vertical-align: middle; padding-top: 0; padding-bottom: 0; height: 40px; line-height: 40px; gap: 6px; margin-left: 20px !important; }
         #megaSearchLaunchBtn .icon { width: 20px; height: 20px; fill: currentColor; }
    `);

    // --- Элементы DOM ---
    let modal, closeBtn, searchInput, searchBtn, sortPriceBtn, sortSalesBtn, sortNameBtn;
    let resultsDiv, statusDiv, excludeInput, addExcludeBtn, exclusionTagsDiv;
    let suggestionsDiv;

    // --- Функции ---

    function formatPrice(priceStr) {
        if (!priceStr) return 0;
        return parseFloat(priceStr.toString().replace(',', '.')) || 0;
     }
    function formatSales(salesStr) {
        if (!salesStr) return 0;
        const cleanedStr = salesStr.toString().replace('+', '').replace(/\s/g, '');
        return parseInt(cleanedStr, 10) || 0;
    }

    // Создание модального окна
    function createModal() {
        modal = document.createElement('div');
        modal.id = 'megaSearchModal';
        const container = document.createElement('div');
        container.id = 'megaSearchContainer';
        closeBtn = document.createElement('button');
        closeBtn.id = 'megaSearchCloseBtn';
        closeBtn.innerHTML = '&times;';
        closeBtn.onclick = hideModal;
        const header = document.createElement('div');
        header.id = 'megaSearchHeader';
        const searchInputContainer = document.createElement('div');
        searchInputContainer.className = 'megaSearchInputContainer';
        searchInput = document.createElement('input');
        searchInput.id = 'megaSearchInput';
        searchInput.type = 'text';
        searchInput.placeholder = 'Введите название игры...';
        searchInput.autocomplete = 'off';
        searchInput.onkeydown = (e) => { if (e.key === 'Enter') triggerSearch(); };
        searchInput.oninput = () => {
            clearTimeout(suggestDebounceTimeout);
            suggestDebounceTimeout = setTimeout(() => {
                fetchSuggestions(searchInput.value);
            }, SUGGEST_DEBOUNCE_MS);
        };
        searchInput.onblur = () => {
             setTimeout(() => {
                 if (suggestionsDiv) suggestionsDiv.style.display = 'none';
             }, 150);
        };
        searchInputContainer.appendChild(searchInput);
        suggestionsDiv = document.createElement('div');
        suggestionsDiv.id = 'megaSearchSuggestions';
        searchInputContainer.appendChild(suggestionsDiv);
        header.appendChild(searchInputContainer);
        searchBtn = document.createElement('button');
        searchBtn.textContent = 'Найти';
        searchBtn.id = 'megaSearchGoBtn';
        searchBtn.className = 'megaSearchBtn';
        searchBtn.onclick = triggerSearch;
        header.appendChild(searchBtn);
        sortPriceBtn = document.createElement('button');
        sortPriceBtn.textContent = 'Цена ▼';
        sortPriceBtn.className = 'megaSearchBtn sortBtn';
        sortPriceBtn.dataset.sort = 'price';
        sortPriceBtn.dataset.dir = 'desc';
        sortPriceBtn.onclick = () => handleSort('price');
        header.appendChild(sortPriceBtn);
        sortSalesBtn = document.createElement('button');
        sortSalesBtn.textContent = 'Продажи ▼';
        sortSalesBtn.className = 'megaSearchBtn sortBtn';
        sortSalesBtn.dataset.sort = 'sales';
        sortSalesBtn.dataset.dir = 'desc';
        sortSalesBtn.onclick = () => handleSort('sales');
        header.appendChild(sortSalesBtn);
        sortNameBtn = document.createElement('button');
        sortNameBtn.textContent = 'Название ▼';
        sortNameBtn.className = 'megaSearchBtn sortBtn';
        sortNameBtn.dataset.sort = 'name';
        sortNameBtn.dataset.dir = 'desc';
        sortNameBtn.onclick = () => handleSort('name');
        header.appendChild(sortNameBtn);
        const filterGroup = document.createElement('div');
        filterGroup.className = 'megaSearchInputGroup';
        excludeInput = document.createElement('input');
        excludeInput.type = 'text';
        excludeInput.id = 'megaSearchExcludeInput';
        excludeInput.placeholder = 'Исключить слово';
        excludeInput.onkeydown = (e) => { if (e.key === 'Enter') addFilterKeyword(); };
        filterGroup.appendChild(excludeInput);
        addExcludeBtn = document.createElement('button');
        addExcludeBtn.id = 'megaSearchAddExcludeBtn';
        addExcludeBtn.innerHTML = `<svg viewBox="0 0 20 20"><path d="M10 2.5a.75.75 0 0 1 .75.75v6h6a.75.75 0 0 1 0 1.5h-6v6a.75.75 0 0 1-1.5 0v-6h-6a.75.75 0 0 1 0-1.5h6v-6a.75.75 0 0 1 .75-.75Z" /></svg>`;
        addExcludeBtn.onclick = addFilterKeyword;
        filterGroup.appendChild(addExcludeBtn);
        header.appendChild(filterGroup);
        container.appendChild(header);
        resultsDiv = document.createElement('div');
        resultsDiv.id = 'megaSearchResults';
        statusDiv = document.createElement('div');
        statusDiv.id = 'megaSearchResultsStatus';
        resultsDiv.appendChild(statusDiv);
        container.appendChild(resultsDiv);
        modal.appendChild(container);
        exclusionTagsDiv = document.createElement('div');
        exclusionTagsDiv.id = 'megaSearchExclusionTags';
        modal.appendChild(exclusionTagsDiv);
        modal.appendChild(closeBtn);
        document.body.appendChild(modal);
    }

    // Показать/скрыть модальное окно
    function showModal() {
        if (!modal) createModal();
        modal.style.display = 'block';
        searchInput.focus();
        document.body.style.overflow = 'hidden';
        renderExclusionTags();
        applyFilters();
    }
    function hideModal() {
        if (modal) {
             modal.style.display = 'none';
             if (suggestionsDiv) suggestionsDiv.style.display = 'none';
        }
        document.body.style.overflow = '';
    }

    // Обновить статус
    function updateStatus(message) {
        statusDiv.textContent = message;
     }

    // Запуск поиска
    function triggerSearch() {
         const query = searchInput.value.trim();
         if (suggestionsDiv) suggestionsDiv.style.display = 'none';
        if (!query) {
            updateStatus('Пожалуйста, введите запрос.');
            return;
        }
        currentResults = [];
        firstSortClick = { price: true, sales: true, name: true, relevance: false };
        $('#megaSearchHeader .sortBtn').removeClass('active').each(function() {
             const baseText = $(this).text().replace(' ▲', '').replace(' ▼', '');
             $(this).text(baseText + ' ▼').attr('data-dir', 'desc');
        });
        renderResults();
        updateStatus('Получение общего количества...');
        fetchTotalCount(query);
    }

    // --- Функции подсказок ---
    function fetchSuggestions(query) {
        const trimmedQuery = query.trim();
        if (trimmedQuery.length < 2) {
            suggestionsDiv.innerHTML = '';
            suggestionsDiv.style.display = 'none';
            return;
        }
        const params = new URLSearchParams({ q: trimmedQuery, v: 2 });
         try {
             if (typeof plang !== 'undefined') params.append('lang', plang);
             if (typeof clientgeo !== 'undefined') params.append('geo', clientgeo);
         } catch (e) { console.warn("Could not get plang/clientgeo for suggestions."); }
        GM_xmlhttpRequest({
            method: "GET",
            url: `${SUGGEST_API_URL}?${params.toString()}`,
            onload: function(response) {
                try {
                    const suggestions = JSON.parse(response.responseText);
                    renderSuggestions(suggestions);
                } catch (e) {
                    console.error("Error parsing suggestions:", e, response.responseText);
                    suggestionsDiv.innerHTML = '';
                    suggestionsDiv.style.display = 'none';
                }
            },
            onerror: function(error) {
                console.error("Error fetching suggestions:", error);
                suggestionsDiv.innerHTML = '';
                suggestionsDiv.style.display = 'none';
            }
        });
    }
    function renderSuggestions(suggestions) {
        if (!suggestions || suggestions.length === 0) {
            suggestionsDiv.innerHTML = '';
            suggestionsDiv.style.display = 'none';
            return;
        }
        suggestionsDiv.innerHTML = '';
        suggestions.forEach(suggestion => {
             if (suggestion.type === "Товары" || suggestion.type === "Search") {
                const item = document.createElement('div');
                item.className = 'suggestionItem';
                item.textContent = suggestion.name;
                item.onmousedown = (e) => {
                    e.preventDefault();
                    searchInput.value = suggestion.name;
                    suggestionsDiv.style.display = 'none';
                    triggerSearch();
                };
                suggestionsDiv.appendChild(item);
            }
        });
        suggestionsDiv.style.display = suggestionsDiv.children.length > 0 ? 'block' : 'none';
    }

    // --- Запросы API ---
    function fetchTotalCount(query) {
         const params = new URLSearchParams({
            query: query, searchmode: 10, sortmode: DEFAULT_SORT_MODE,
            pagesize: RESULTS_PER_PAGE_CHECK, pagenum: 1, owner: 1,
            details: 1, checkhidesales: 1, host: 'plati.market'
        });
        GM_xmlhttpRequest({
            method: "GET", url: `${API_BASE_URL}?${params.toString()}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data?.result?.total > 0) {
                        const total = data.result.total;
                        updateStatus(`Найдено ${total} товаров. Загрузка...`);
                        fetchAllResults(query, total);
                    } else {
                         updateStatus(`По запросу "${query}" ничего не найдено.`);
                         currentResults = []; renderResults();
                    }
                } catch (e) {
                    console.error("Ошибка парсинга ответа (количество):", e, response.responseText);
                    updateStatus('Ошибка получения общего количества товаров.');
                }
            },
            onerror: function(error) {
                console.error("Сетевая ошибка (количество):", error);
                updateStatus('Ошибка сети при получении общего количества товаров.');
            }
        });
    }
    function fetchAllResults(query, total) {
         const params = new URLSearchParams({
            query: query, searchmode: 10, sortmode: DEFAULT_SORT_MODE,
            pagesize: total, pagenum: 1, owner: 1,
            details: 1, checkhidesales: 1, host: 'plati.market'
        });
         GM_xmlhttpRequest({
            method: "GET", url: `${API_BASE_URL}?${params.toString()}`,
            timeout: 60000,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data?.items?.item) {
                        currentResults = data.items.item;
                         updateStatus(`Загружено ${currentResults.length} из ${total} товаров.`);
                         applySort(currentSort.field, currentSort.direction);
                         renderResults();
                    } else {
                        updateStatus(`Ошибка загрузки товаров. Ответ API не содержит данных.`);
                        currentResults = []; renderResults();
                    }
                } catch (e) {
                    console.error("Ошибка парсинга ответа (все):", e, response.responseText);
                    updateStatus('Ошибка обработки данных товаров.');
                     currentResults = []; renderResults();
                }
            },
            onerror: function(error) {
                console.error("Сетевая ошибка (все):", error);
                updateStatus('Ошибка сети при загрузке всех товаров.');
                 currentResults = []; renderResults();
            },
             ontimeout: function() {
                 console.error("Таймаут загрузки всех результатов");
                updateStatus('Время ожидания ответа от сервера истекло.');
                 currentResults = []; renderResults();
             }
        });
    }

    // --- Сортировка ---
    function handleSort(field) {
         let newDirection;
        const currentBtn = $(`#megaSearchHeader .sortBtn[data-sort="${field}"]`);
        const currentDir = currentBtn.attr('data-dir');
        if (firstSortClick[field]) {
            if (field === 'price' || field === 'name') { newDirection = 'asc'; }
            else { newDirection = 'desc'; }
            firstSortClick[field] = false;
        } else {
            newDirection = currentDir === 'desc' ? 'asc' : 'desc';
        }
        $('#megaSearchHeader .sortBtn').removeClass('active').each(function() {
            if ($(this).data('sort') !== field) {
                 const baseText = $(this).text().replace(' ▲', '').replace(' ▼', '');
                 $(this).text(baseText + ' ▼').attr('data-dir', 'desc');
                 firstSortClick[$(this).data('sort')] = true;
            }
        });
        const arrow = newDirection === 'asc' ? ' ▲' : ' ▼';
        currentBtn.text(currentBtn.text().replace(' ▲', '').replace(' ▼', '') + arrow);
        currentBtn.addClass('active');
        currentBtn.attr('data-dir', newDirection);
        currentSort.field = field;
        currentSort.direction = newDirection;
        applySort(field, newDirection);
        renderResults();
    }
    function applySort(field, direction) {
         const dirMultiplier = direction === 'asc' ? 1 : -1;
        currentResults.sort((a, b) => {
            let valA, valB;
            switch (field) {
                case 'price':
                    let priceARur = formatPrice(a.price_rur); let priceBRur = formatPrice(b.price_rur);
                    let priceAUsd = formatPrice(a.price_usd); let priceBUsd = formatPrice(b.price_usd);
                    valA = priceARur > 0 ? priceARur : (priceAUsd > 0 ? priceAUsd * 100 : Infinity);
                    valB = priceBRur > 0 ? priceBRur : (priceBUsd > 0 ? priceBUsd * 100 : Infinity);
                    break;
                case 'sales': valA = formatSales(a.cnt_sell); valB = formatSales(b.cnt_sell); break;
                case 'name': valA = (a.name || '').toLowerCase(); valB = (b.name || '').toLowerCase(); break;
                case 'relevance': default: return 0;
            }
            if (valA < valB) return -1 * dirMultiplier;
            if (valA > valB) return 1 * dirMultiplier;
            return 0;
        });
    }

    // --- Фильтрация (Исключения) ---
    function addFilterKeyword() {
         const keyword = excludeInput.value.trim().toLowerCase();
        if (keyword && !exclusionKeywords.includes(keyword)) {
            exclusionKeywords.push(keyword);
            GM_setValue('megaSearchExclusions', exclusionKeywords);
            excludeInput.value = '';
            renderExclusionTags();
            applyFilters();
        }
     }
    function removeFilterKeyword(keywordToRemove) {
        exclusionKeywords = exclusionKeywords.filter(k => k !== keywordToRemove);
        GM_setValue('megaSearchExclusions', exclusionKeywords);
        renderExclusionTags();
        applyFilters();
    }
    function renderExclusionTags() {
        if (!exclusionTagsDiv) return;
        exclusionTagsDiv.innerHTML = '';
        exclusionKeywords.forEach(keyword => {
            const tag = document.createElement('span');
            tag.className = 'exclusionTag';
            tag.textContent = keyword;
            tag.title = `Удалить "${keyword}"`;
            tag.onclick = () => removeFilterKeyword(keyword);
            exclusionTagsDiv.appendChild(tag);
        });
    }
    function applyFilters() {
        const keywords = exclusionKeywords;
        let visibleCount = 0;
        $('.megaSearchItem').each(function() {
            const itemElement = $(this);
            const title = itemElement.find('.title').text().toLowerCase();
            const seller = itemElement.find('.seller').text().toLowerCase();
            const itemText = title + ' ' + seller;
            let shouldHide = false;
            if (keywords.length > 0) {
                shouldHide = keywords.some(keyword => itemText.includes(keyword));
            }
            if (shouldHide) { itemElement.addClass('hidden-by-filter'); }
            else { itemElement.removeClass('hidden-by-filter'); visibleCount++; }
        });
        const totalCount = currentResults.length;
        if (keywords.length > 0 && totalCount > 0) {
              updateStatus(`Показано ${visibleCount} из ${totalCount} товаров (${keywords.length} искл.).`);
        } else if (totalCount > 0) {
            updateStatus(`Загружено ${totalCount} товаров.`);
        } else if (statusDiv.textContent.includes('Загрузка') || statusDiv.textContent.includes('Найдено')) { }
        else if (searchInput.value.trim()){ updateStatus(`По запросу "${searchInput.value}" ничего не найдено.`); }
        else { updateStatus(`Введите запрос для поиска.`); }
    }

    // --- Рендеринг Результатов ---
    function renderResults() {
         resultsDiv.innerHTML = '';
        resultsDiv.appendChild(statusDiv);
        if (currentResults.length === 0 && !statusDiv.textContent.includes('Найдено') && !statusDiv.textContent.includes('Загрузка')) {
             updateStatus(searchInput.value.trim() ? `По запросу "${searchInput.value}" ничего не найдено.` : `Введите запрос для поиска.`);
             return;
        }
         if (currentResults.length === 0 && (statusDiv.textContent.includes('Найдено') || statusDiv.textContent.includes('Загрузка'))) {
            return; // Не перезаписываем статус пока идет загрузка или если уже написано "Найдено 0"
        }

        currentResults.forEach(item => {
            const itemDiv = document.createElement('div');
            itemDiv.className = 'megaSearchItem';
            itemDiv.dataset.id = item.id;
            const link = document.createElement('a');
            link.href = item.url || `https://plati.market/itm/${item.id}`;
            link.target = '_blank';
            const img = document.createElement('img');
            const imgSrc = `//${IMAGE_DOMAIN}/imgwebp.ashx?id_d=${item.id}&w=164&h=164&dc=${item.ticks_last_change || Date.now()}`;
            img.src = imgSrc;
            img.alt = item.name || 'Product image';
            img.onerror = function() { this.src = 'https://plati.market/images/logo-plati.png'; };
            link.appendChild(img);
            const priceDiv = document.createElement('div');
            priceDiv.className = 'price';
            let displayPrice = formatPrice(item.price_rur); let currencySymbol = '₽';
            if (displayPrice <= 0) { displayPrice = formatPrice(item.price_usd); currencySymbol = '$'; }
            if (displayPrice <= 0) { displayPrice = formatPrice(item.price_eur); currencySymbol = '€'; }
            if (displayPrice <= 0) { displayPrice = formatPrice(item.price_uah); currencySymbol = '₴'; }
            priceDiv.textContent = displayPrice > 0 ? `${displayPrice.toLocaleString('ru-RU')} ${currencySymbol}` : 'Нет цены';
            link.appendChild(priceDiv);
            const titleDiv = document.createElement('div');
            titleDiv.className = 'title';
            titleDiv.textContent = item.name || 'Без названия';
            titleDiv.title = item.name || 'Без названия';
            link.appendChild(titleDiv);
            const sellerDiv = document.createElement('div');
            sellerDiv.className = 'seller';
            sellerDiv.textContent = `Продавец: ${item.seller_name || 'N/A'}`;
             sellerDiv.title = `Продавец: ${item.seller_name || 'N/A'}`;
            link.appendChild(sellerDiv);
            const salesDiv = document.createElement('div');
            salesDiv.className = 'sales';
            let salesCount = formatSales(item.cnt_sell);
            salesDiv.textContent = `Продано: ${salesCount > 0 ? salesCount.toLocaleString('ru-RU') : '0'}`;
            link.appendChild(salesDiv);
            const buyButtonDiv = document.createElement('div');
            buyButtonDiv.className = 'buyButton';
            buyButtonDiv.textContent = 'Перейти';
            link.appendChild(buyButtonDiv);
            itemDiv.appendChild(link);
            resultsDiv.appendChild(itemDiv);
        });
        applyFilters();
    }

    // --- Инициализация ---
    function init() {
        const logoLink = document.querySelector('a[href="/"][class*="order-xl-1"]');
        if (logoLink) {
            const megaSearchButton = document.createElement('button');
            megaSearchButton.className = 'button button—accent button—medium';
            megaSearchButton.id = 'megaSearchLaunchBtn';
            megaSearchButton.innerHTML = `
                <svg class="icon" width="20" height="20">
                    <use xlink:href="/build/sprite.svg#loupe"></use>
                </svg>
                <span style="margin-left: 6px;">MegaSearch</span>
            `;
            megaSearchButton.onclick = showModal;
            logoLink.parentNode.insertBefore(megaSearchButton, logoLink.nextSibling);
        } else {
            console.warn("MegaSearch Script: Не найден элемент логотипа для добавления кнопки.");
        }
    }

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

})();

 

 

Изменено пользователем 0wn3df1x
  • Спасибо (+1) 1

Поделиться сообщением


Ссылка на сообщение

@0wn3df1x А по цене, как отфильтровать на сайте, без скрипта, я так и не понял, на новой версии?

Поделиться сообщением


Ссылка на сообщение
37 минут назад, Alex Po Quest сказал:

@0wn3df1x А по цене, как отфильтровать на сайте, без скрипта, я так и не понял, на новой версии?

Никак. На старой версии версии сайта ведь тоже нельзя было. Одна из причин написания скрипта.

Изменено пользователем 0wn3df1x

Поделиться сообщением


Ссылка на сообщение

Кстати, а есть скрипт или расширение, отсортировать, что интересного выходит, но не переведено на русский из новинок?, а то вручную не удобно смотреть.

  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение
29 минут назад, 0wn3df1x сказал:

Никак. На старой версии версии сайта ведь тоже нельзя было. Одна из причин написания скрипта.

Понятно. Теперь неудобно смотреть. Текст обрезается и не прочтёшь. Раньше было удобнее.

P.S. Хочешь не хочешь - но надо скрипт попробовать.

Поделиться сообщением


Ссылка на сообщение

Кстати да. Есть скрипты, показывающие помимо официального перевода на русский возможность установить перевод? Думаю, была бы удобная фишка.

Поделиться сообщением


Ссылка на сообщение

Создайте аккаунт или войдите в него для комментирования

Вы должны быть пользователем, чтобы оставить комментарий

Создать аккаунт

Зарегистрируйтесь для получения аккаунта. Это просто!

Зарегистрировать аккаунт

Войти

Уже зарегистрированы? Войдите здесь.

Войти сейчас



Zone of Games © 2003–2025 | Реклама на сайте.

×