Перейти к содержимому
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. Хочешь не хочешь - но надо скрипт попробовать.

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


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

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

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


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

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

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

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

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

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

Войти

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

Войти сейчас


  • Сейчас популярно

  • Продвигаемые темы

  • Последние сообщения

    • Я с планшета пишу и фоткаю, не с телефона
    • Безобразие, кошмар! Как так можно то? Половина игр *Ой, извините!".
    • Может кто скинуть ссылку на ютуб-видео, с любительской многоголосой AI-озвучкой PS2-игры, так скажем, на образец для подражания?
    • Нет, мне нужно знать, какой именно функционал должен присутствовать в софте и тестировщики, которые будут отлавливать ошибки demo-версии софта, тоже бы не помешали. Конечно он должен быть совместим с PS2-играми, так, как я фан конкретно PS2. Именно распаковщики-запаковщики типа Sky`s Console File Scanner.exe ( извлечение графики из Ratchet например, звук и.т.д., также умеет вставлять файлы назад), QuickBMS, Xpert2, я раньше не писал. 
    •   Ты прав в своих сомнениях. NoNpDrm не имеет никакого отношения к проверке подписи исполняемых файлов (SELF).   NoNpDrm генерирует фейковый файл лицензии (work.bin / .rif), чтобы обойти DRM-проверку при запуске игры. За проверку подписи самого eboot.bin (SELF) отвечает ядро (SceKernel). Если ты заменишь оригинальный retail-SELF на свой пропатченный ELF, ядро это увидит, поймет, что хэш не совпадает с подписью Sony, и уйдет в панику (краш, ошибка C2-12828-1 или просто черный экран).   Твой инстинкт «сделать через изменение данных» был абсолютно верным. Данные (тексты, видео, звуки) не подписываются. Но для 60 FPS патча и переноса читов с PS3 нам придется лезть в код. И вот как это сделать правильно, не мучаясь с подписями.   Решение №1: Как правильно внедрить ELF и забыть о подписях (FSELF + rePatch)   Если ты хочешь именно заменить бинарник, тебе не нужно пытаться «переподписать» его ключами Sony (это невозможно). Тебе нужно заставить консоль принимать неподписанные файлы.   Дешифровка и правка:   Используешь FAGDec или VitaDumper, чтобы получить чистый расшифрованный eboot.bin (в формате ELF). Вносишь изменения в ELF (через IDA Pro / Ghidra).   Сборка в FSELF (Fake SELF):   Используешь vita-make-fself (из vitasdk), чтобы превратить свой ELF в eboot.bin с пустой (фейковой) подписью.   Обход проверки ядра (Самое важное!):   Чтобы консоль проглотила FSELF вместо Retail SELF, тебе нужен плагин, который патчит ядро. 0syscall6 — хороший вариант, он патчит ScePaf и SceKernel, отключая проверки. rePatch-reDux0 (или reF00D) — ИДЕАЛЬНЫЙ ВАРИАНТ. Этот плагин позволяет тебе просто кинуть твой пропатченный eboot.bin в папку ux0:rePatch/PCSG00XXX/ (где PCSG00XXX — Title ID игры). Плагин сам перехватит загрузку модулей и подсунет игре твой FSELF, полностью игнорируя оригинальный подписанный файл. Тебе даже не придется каждый раз перепаковывать игру!   Решение №2: Runtime Hooking (Золотой стандарт для 60 FPS и читов)   Если ты хочешь сделать качественный 60 FPS патч и портировать читы с PS3, лучше вообще не трогать eboot.bin. Вместо внедрения в SELF, мы пишем плагин для taiHEN (.suprx), который внедряется в оперативную память игры во время её работы.   Почему это круче:   Оригинальный eboot.bin остается нетронутым. Никаких проблем с подписями, NoNpDrm и FSELF. Портировать с PS3 проще: Ты не ищешь инструкции ARM, ты просто перехватываешь функции по адресам в памяти. Универсальность: Один .suprx плагин можно обновлять без переустановки игры.   Как это сделать:   Берешь шаблон плагина taiHEN (на C/C++). Используешь taiHookFunctionOffset или taiHookFunctionExport. Для 60 FPS: Находишь в игре функцию, отвечающую за vblank (вертикальную синхронизацию) или внутренний счетчик кадров (Frame Pacing), и хукаешь её, заставляя возвращать 1 вместо 2 (для 30->60 FPS) или патчишь float значение delta_time. Для читов: Перехватываешь функции обновления HP, таймеров и т.д. прямо в RAM.   Перенос наработок с PS3 на Vita (Архитектурный ад, но решаемый)   Ты упомянул: "вся работа между ps3 и psvita, в теории, легко переносится". Осторожно: это не совсем так.   PS3 — это архитектура PowerPC (Cell Broadband Engine), Big-Endian. PS Vita — это ARM Cortex-A9, Little-Endian.   Как переносить:   Бинарный код не перенести. Ассемблер PPC и ARM Thumb/ARM32 — это разные языки. Переносится ЛОГИКА и АЛГОРИТМЫ. Если на PS3 ты нашел, что игра читает таймер из адреса 0x81234567 и умножает на 0.5, то на Вите тебе нужно открыть отладчик (VitaCheat через FTP или Cheat Engine через плагин), найти эту же переменную в памяти Vita и написать патч уже под ARM-адрес. Читы с PS3 служат тебе отличной «картой». Ты знаешь, что искать (например, "значение таймера находится рядом с указателем на объект игрока"), и просто ищешь этот же паттерн в дампе памяти Vita.   Решение для Видео: Апскейл, 60 FPS и Субтитры   Если думаешь отказаться от хардсабов (вшивания текста в видео) и озвучки, это огромный плюс к качеству. Чистое видео + софтсабы (текстовые субтитры движка) выглядят на OLED-экране Vita шикарно.   Как сделать крутые 60 FPS видеоролики:   Извлечение: Достаешь оригинальные ролики (обычно .pam, .mp4 или .usm). Конвертация в 60 FPS (Интерполяция): Простое увеличение FPS с 30 до 60 сделает видео дерганым. Тебе нужен Optical Flow (оптический поток) для генерации промежуточных кадров. Используй FFmpeg с фильтром minterpolate:   ffmpeg -i input_30fps.mp4 -vf "minterpolate='mi_mode=mci:mc_mode=aobmc:vsbmc=1:fps=60'" -c:v libx264 -profile:v high -level 4.0 -crf 18 output_60fps.mp4   (Vita обожает H.264 High Profile Level 4.0. Битрейт можно задирать до 10-15 Mbps, аппаратный декодер Vita это переварит).   Апскейл (Опционально): Если оригинал 720x408, можно аккуратно апскейлнуть до 960x544 (родное разрешение Vita) через Topaz Video AI или FFmpeg с lanczos, но иногда оригинал с легким блюром выглядит лучше, чем мыльный апскейл. Замена: Кидаешь новые видео в папку с игрой (через rePatch или прямую замену в app/). Подпись не нужна! Субтитры: Так как хардсабов нет, тебе нужно будет перевести и зашить текст в текстовые базы игры (обычно это .gxt файлы или проприетарные архивы). Для этого используются скрипты на Python (QuickBMS) для распаковки и утилиты вроде GXTConvert. Движок игры сам наложит идеальный, четкий текст поверх твоего 60 FPS видео.   Итоговый Action-Plan:   Забудь про мучительную переподпись eboot.bin. Используй taiHEN для написания .suprx плагинов (Runtime Hooking). Если все же нужно патчить сам ELF — используй связку FAGDec + vita-make-fself + rePatch-reDux0. Тестирование: Используй VitaShell и FTP для быстрой замены файлов и .suprx плагинов без переустановки VPK.
    • Tirniel, ты, конечно, стараешься выглядеть экспертом, но в каждом твоём тезисе — дыра размером с консоль. Давай разберём по пунктам, без воды, с фактами. «Единый движок — это недостижимо, универсальный комбайн — фантазия» Слушай, это смешно. Ты пишешь, что «это недостижимо», а я сейчас тебе покажу работающий инструмент, который делает ровно то, что ты отрицаешь. Wrench Editor — это набор моддинг-тулов для всех четырёх PS2-игр Ratchet & Clank: R&C1, R&C2, R&C3 и Deadlocked. Один софт — под четыре игры. И это не просто «парсер текста», а полноценный редактор уровней, моделей и ресурсов. Теперь про «движки». Первая часть Ratchet & Clank использует модифицированный движок Kinetica. А дальше Insomniac Games и Naughty Dog имели соглашение о свободном обмене технологиями — они делились наработками и оптимизациями между студиями. Это не «разные движки с нуля», а эволюция одной технологической базы. Так что твой тезис про «недостижимость» разбивается о репозиторий на GitHub, который обновлялся всего пару месяцев назад. Не знал? Бывает. «Шрифты могут быть запакованы так, что их не найти — кастомные решения неизбежны» Окей, допустим, шрифты бывают сложными. Но это не значит, что для каждой игры нужно писать велосипед. Возьми LunaTranslator — открытый инструмент, который поддерживает HOOK-эмуляторы для NS, PSP, PS Vita и PS2 и позволяет извлекать текст напрямую из памяти большинства игр на этих платформах. Он работает с PPSSPP (PSP), Vita3K (PS Vita) и другими эмуляторами. То есть проблема извлечения текста и работы с ресурсами уже решена для большинства игр. Не надо городить кастомные костыли — есть готовые инструменты, которые просто берут и делают. «Нейронка пишет чушь, она не способна выполнить сложную задачу» Ты, видимо, застрял в 2020 году. Нейросети — это инструмент, как молоток. Молоток сам дом не построит, но без него ты будешь забивать гвозди кулаком. Никто и не говорит, что нейронка сама всё сделает. Речь о конвейере: LunaTranslator извлекает текст через HOOK. Ты переводишь этот текст (хоть нейросетью, хоть вручную). Тот же инструмент или аналогичный вставляет перевод обратно. Это не «фантазии», это рабочие решения, которые уже используют сотни людей. Просто ты о них не знаешь, потому что не гуглил. «Куда проще создать прогу с нуля, чем разбираться в чужом легаси-коде» Это, пожалуй, твой самый убийственный для себя тезис. Ты предлагаешь переписывать с нуля то, что уже работает. Давай посчитаем: Wrench Editor — это годы работы сообщества. Там учтены сотни нюансов, о которых ты даже не догадываешься. LunaTranslator — это тысячи человеко-часов разработки и отладки. Apollo Save Tool для PS Vita — позволяет переподписывать файлы сохранений прямо на консоли. Тоже не «догадки», а работающий код. И ты предлагаешь взять и переписать это всё с нуля? Серьёзно? Ты, наверное, тот самый программист, который в каждом проекте переписывает printf, потому что «чужой код — говно». В индустрии это называется not invented here syndrome. И это не признак профессионализма, а признак того, что ты не умеешь работать в экосистеме. Ты предлагаешь писать отдельный софт под каждую игру — это ремесленничество. Инженер берёт готовые модули и склеивает конвейер. А ты боишься, что твои уникальные навыки ручного патчинга станут никому не нужны — поэтому и отрицаешь очевидное. «Попробуй сам» — ок, я возьму эти тулы и попробую. А ты попробуй открыть гугл, прежде чем рассуждать о недостижимом. Пока — твои «факты» — это просто страх, прикрытый пафосом. Я хочу, чтобы пещерные переводчики, которые озвучивают до сих пор одноголосо через сайт, текст → голос и запихивают эту озвучку обратно в игру, шли в ногу со временем и использовали современные нано-технологии, AI-решения, на максимум! Иначе, чем вы лучше пиратов, которые переводили интерфейс игр для PS2, от такого русского интерфейса, игра приятней не становилась.
    • Еду,Че на нее смотреть,её  есть надо!
    •   Ты говоришь: «Насчёт подписей на вите не знаю, ибо никогда не делал» И это видно. Ты даже не удосужился загуглить, а уже строишь предположения. На самом деле, подписывать софт на PS Vita умеют даже школьники, которые вчера скачали хенкаку. Есть куча готовых инструментов: Apollo Save Tool — приложение для PS Vita, которое позволяет переподписывать файлы сохранения прямо на консоли. fake_np — утилита, которая конвертирует ISO и homebrew в подписанные файлы для запуска на Vita И это только верхушка айсберга. Есть целая экосистема SELF / Fake SELF (FSELF) — нативные исполняемые файлы Vita, для которых создаются фейковые подписи. Это стандартная практика для homebrew-разработки. Так что твоё «скорее всего что-то подобное должно быть» — это не догадка, а банальное незнание того, что уже давно существует и работает. Открой гугл, почитай вики, посмотри GitHub. А потом уже пиши, что «ерунда», а что нет. А пока — твои «догадки» и «скорее всего» — это просто попытка скрыть своё незнание за пафосными фразами.   Скажи это разрабам Ratchet & Clank: Rift Apart или ты думаешь, что умнее их?) Игра станет весить в два раза больше?
    • Вкусовщина чистой воды. Есть уйма людей, которые напротив предпочитают субтитры, либо сабы И озвучку, т.к. можно услышать не всё или не всё разобрать.
  • Изменения статусов

    • Perfect End Strange  »  SerGEAnt

      Привет, озвучку для Silent Hill 4: The Room выпустил энтузиаст под ником Perfect End Strange, а вы написали совсем другую студию, пожалуйста, исправьте эту оплошность
      · 0 ответов
    • Jimmi Hopkins  »  SerGEAnt

      я его уже открыл в свободный доступ, чтобы не потерялось среди новинок
      · 0 ответов
    • NekoNya  »  Onzi

      https://www.dropbox.com/scl/fi/4xncxr2tqy77vz4644q08/text_RU_FINAL.json?rlkey=xckxss3jkql4gjmc1ah6qjb5e&st=k5ql651f&dl=0 
       
      Вот, то что вышло. Извинюсь, что зря говорил, что помогу (
      · 2 ответа
    • serfly  »  SerGEAnt

      Доброго времени! Недавно столкнулся с проблемой автоперевода XUnity AutoTranslator BeplnEx iL2cc. Пару последних лет часто пользовался, теперь консольные команды виснут на стадии загрузки библиотек, хотя они есть там изначально. На GitHub есть подобные вопросы, но ни одного рабочего ответа я не нашел. А Вы что можете посоветовать?
      · 0 ответов
    • serfly  »  SamhainGhost

      Доброго времени. По локализации игры Wicked Seed. Шрифт размазан и в полях, другие можно попробовать? Например arialuni_sdf_2022? Я профан в этом, но интересно.
       
      · 0 ответов
  • Лучшие авторы


×