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

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


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

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

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


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

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

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

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

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

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

Войти

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

Войти сейчас


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

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

    • Под ними ты похоже понимаешь шулеров. Какое они имеют отношение к дилерам казино?
    • Так это 8-ым шрифтом. К тому же не путай, не 17 страниц, а 17 листов, а это уже 34 страницы.
    • Вот-вот. Сержант с батлой уже второй раз подставляет. То от ролика лишь 20 секунд с датой релиза выложит, то скромно умолять о том, что игроки были предупреждены. 
    • То, что  у некоторой части “счастливых” обладателей игры, она не запустилась по причине "DirectX 12 is not supported on your system" - то это нормально. На это закрывают глаза. Все хорошо, прекрасная маркиза. За исключеньем пустяка.
    • Ну, два года для сферы разработок — это так-то весьма скромные сроки. За это время не всегда завершают даже фазы концептов в случае крупных студий. Так что пилить скорее всего будут ещё не один год, собственно, вот туда эти деньги, выделенные на разработку, и уйдут. Ну ещё бы, в кои-то веки вышла игра не за 3-4 тысячи, а за одну. Не за 50-60 баксов, как уже можно было бы ожидать на фоне других крупных проектов, а за двадцатку для остальных, а то и дешевле. Хороший пример тому, что ценовая политика как минимум благотворно сказывается на числе проданных копий. Другое дело, что профит ещё только предстоит рассчитать по завершению первого месяца продаж, т.к. пока что игра, судя по всему, ещё даже и не окупилась для них.
    • Ну зачем же сразу “нелицензионные”. Это можно провернуть более чем штатными средствами от самих же майков вполне легальным образом на лицензионном образе, скачанном с сайта майков. А потом просто не активировать винду, любуясь на надпись в правом нижнем углу, от которой особенно не горачо и не холодно, разве что обои на рабочем столе штатными средствами не поменять — вот и всё. Ну тогда “ладно”, допустим, у людей хотя бы было время. Вот оно что бывает, когда новость даёт неполную информацию. Так вот вышел срач, а иначе бы некоторые участники диалога (например, я сам) просто пожали бы плечами и пошли дальше (возможно, не факт, вряд ли). Ну про то, что до того же стима ему далеко, никто и не спорит. Но всё-таки и того, что там вот прям совсем ничего не улучшается — тоже сказать язык не поворачивается. Разве что темпы развития у него просто улиточными темпами продвигаются, даже не черепашьими шагами. Эмм, покупал не так давно у эпиков через карту озона. Сколько было написано — столько и заплатил. Откуда там двойная конвертация? В стиме же всегда надо кидать с запасом из-за неё, чтобы хватило, да ещё и до кучи подсчитывать высвечиваемые баксы, переводя их по курсу самого стима (до оплаты, чтобы уж наверняка было достаточно), что тоже дополнительное телодвижение.   Ну, собственно, ты сказал, что игра-то ведь работает (подчеркнул в цитате), это лишь античит не работает. То есть пусть он и блокирует, но ты явно указал, что игра сама по себе работает. Но т.к. античит не даёт ей запускаться, то очевидно, что она не может работать при этом. Как-то так. Ну всё не настолько уж плохо. Системы без крипточипа могут быть и вполне себе новыми, просто чипа не завезли, либо он тупо бракованный (а такое тоже не редкость, особенно если на материнке человек экономил). Молодые люди тоже могут быть вполне и не бум-бум в данной области. Это так-то более чем обычное явление. Есть прорва людей, котоыре относятся к пк так же, как к консоле, то есть на уровне нажал кнопку и играешь, а в нюансы какие-либо не вникают совсем. Много ли ты знаешь людей, которые умеют сами обслуживать те же консоли? А почему по-твоему работают успешно сервисы по обслуживанию пк? Скажи ещё, что люди массово не таскают туда системники для замены термопасты и по прочим причинам (в том числе и таким, что хоть стой, хоть падай — могли бы сами загуглить за минуту, действительно). Проще говоря, в мире хватает людей, которые не являются тобой и не лезут в гугл, пытаясь решить всякое разное самостоятельно — “им и так хорошо”.
    • Приветствую. Если под последней версией гог подразумевается 1.13, то у меня перевод на версию 1.10 от гог. На момент публикации перевода я не знал о существовании версии 1.13.
      У меня перевод установился на 1.13 гог, полет нормальный.
      При установке на игру версии 1.13 не нужно создавать ярлык на рабочий стол. В меню настроек версия игры будет обозначена как 1.10.
    • @Freeman665 Да, проблемы с КоД-ом были всегда за последние лет 10 точно. Просто они раньше в меньшей степени выражались и решались проще, чаще сменой настроек в самой игре. То, что они меняют циферки типа у движка, толку никакого нет. Это всё также старое двигло, которое просто обрастает новыми функциями. Его постоянно модифицируют, а не переписывают с нуля, от чего стабильность его шатает во все стороны. Поэтому причины от того, что у тебя игра то не запускается, то постоянно вылетает, могут быть много. При чём одна таже ошибка, а причин по которой она может вылетает много. Слишком чувствителен движок ко всякой херне, когда другим двиглам на всё пофиг. Тут тебе целый набор может быть, от чего у тебя игра вылетает? Возможно дело в драйверах под определённую карту, какие-нибудь приложения в фоне, разгон оперативы, разгон процессора, оперативная память стоит не в тех слотах, включена какая-то функция в биосе. Вот сиди и перебирай, что именно вызывает конфликт с движком, что его колбасит и тебя выбрасывает, когда другим играм всё “ок” и не вызывает никаких проблем со стабильностью. Кривой движок, которые требует давно пойти на пенсию. Но, что раньше Активижен, что сейчас Майкрософт, им пофиг на проблемы двигла, главное успевать капусту стричь с игры, а игроки сами разберутся методом тыка, чтобы игра перестала конфликтовать с чем-то и стала работать стабильно. Мне лично лень с этим говном возится, поэтому тупо снёс. Хотел просто почилить пару вечерков, а в итоге почти заставили заниматься мазохизмом. Хорошо, что на это говно деньги не пришлось тратить.  Учитывая тот мазохизм который они устраивают для игроков, возможно там работают ещё те заднеприводные извращенцы. 
    • Я сам офигел  Установил, смотрю, а у меня там уже достижения есть, сразу вспомнил, что уже запускал и довольно быстро удалил, так как не особо производительность зашла. В общем опять выставляю макс настройки, запускаю, а fps в космос(120+), я уже обрадовался, не зря значит думаю карту обновил ) Играю такой довольный, и вдруг обращаю внимание, что отражения растворяются, да и выглядит всё как-то не очень. Начал разбираться, а оно вона как оказалось .  Давай в стиме смотреть, чё они там наобновляли, ну и всё встало на свои места. А в ruiner играл? Офигенная игруха, в меру быстрая, с отличным музоном и очень крутая стилистически. Я бы на эту демку и внимания не обратил, если б она не от разрабов руинера была.
    • [v16521.r01]
      — Обновлён текст перевода под новую версию игры 16521
  • Изменения статусов

    • Дмитрий Соснов  »  Tirniel

      Привет! ты разбираешься в компьютерном железе, сможешь помочь с советом по апгрейду старого компа?
      · 1 ответ
    • SHAMAH

      Куда вход на сайт убрали и ЗАЧЕМ? Хотел файл скачать, там только медленная загрузка и “зарегистрируйтесь”. Все. Пришлось вручную страницу входа прописывать.
      · 0 ответов
    • Nosferatu  »  behar

      Добрый вечер.
      Подскажите пожалуйста, у вас не осталось случайно исходников для фикса на широкоформатные мониторы для игры Vampire The Masquerade Redemption?
      Если да, то не могли бы вы ими поделиться, а если нет, то прошу прощенья что побеспокоил.
      Заранее спасибо.
      · 0 ответов
    • AlcoKolyic  »  makc_ar

      Здраствуйте! Извините, а можно попросить ссылку на место где можно взять перевод (патч или образ игры с переводом) El Shaddai: Ascension of the Metatron для ps3, пожалуйста? А то в теме к этой игре у меня не получилось найти работающие ссылки… Первая ведет в группу в которой удалены большинство постов, а пост с этой игрой ведет на сайт https://psnext.ru который сейчас не имеет отношения к видеоиграм. 
      · 0 ответов
    • oleg72  »  Boor

      https://www.skidrowcodex.net/fate-reawakened-goldberg/
      · 0 ответов
  • Лучшие авторы


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

×