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

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

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

Ultimate Steam Enhancer || Tamper Monkey

Ultimate Steam Enhancer — это мощный пользовательский скрипт для платформы Steam, расширяющий стандартные возможности магазина и сообщества. Он объединяет в себе функционал множества инструментов, предоставляя пользователю расширенный контроль и доступ к дополнительной информации об играх, ценах, переводах, обзорах, активности друзей и многом другом.

Скрипт соединяет в себе функционал из:


В связи с увеличением объёма и для удобства последующих обновлений, полное описание всех функций было перенесено на GitHub Pages.

 

Код скрипта на GreasyFork

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

 

 

 

 

 

 

 

 

Что нового в версии 2.1.5

Многие из нас сталкивались с несовершенством системы меток в Steam. Разработчик может указать до 20 меток для своей игры, из которых первые 5 считаются основными и наиболее весомыми. При этом сообщество игроков может голосовать и влиять на положение этих меток в общем списке, повышая или понижая их релевантность.

В теории это звучит неплохо, но на практике система часто даёт сбой. Из-за шутников или скоординированных "рейдов" игры начинают получать метки, которые совершенно их не характеризуют. В результате милая головоломка про котят может внезапно получить тег «Психологический хоррор», а хардкорный симулятор выживания — метку «Казуальная игра».

Эти «шуточные» метки обычно не имеют большого веса, но всё равно попадают в двадцатку. Из-за этого, когда вы ищете в магазине хорроры, Steam подсовывает вам ту самую игру про котят, а в подборку казуальных игр попадает хардкорный выживач. Это засоряет поиск и мешает находить то, что действительно нужно.

Чтобы решить эту проблему и вернуть контроль над поиском в ваши руки, я добавил в скрипт новый инструмент — «Умные метки».

Его суть проста: он позволяет фильтровать игры не просто по наличию метки, а по её весомости. Теперь вы можете сказать скрипту: «Покажи мне только те игры, где метка „Рогалик“ входит в топ-7 самых релевантных». Это позволяет мгновенно отсечь все проекты, где этот тег был добавлен случайно или ради шутки и не является для игры определяющим. Точно так же вы можете и полностью скрыть игры, имеющие определённые метки, чтобы они не мешались в результатах.

С этим обновлением ваш поиск станет значительно чище и точнее.


Новые возможности

Новый фильтр «Умные метки» в Каталоге

Что это? В панель фильтров на странице поиска по каталогу добавлена новая секция «Умные метки». Этот инструмент даёт беспрецедентный контроль над результатами поиска.

Как это работает?

  • При нажатии на кнопку «Настроить метки...» открывается модальное окно, где можно задать два типа правил:
  • Активные фильтры (по приоритету): Позволяют отображать только те игры, у которых определённая метка находится в числе первых N. Например, можно показать только те игры, где метка «Рогалик» входит в топ-5 меток.
  • Скрытые фильтры (на исключение): Позволяют полностью скрыть из результатов игры, имеющие хотя бы один из указанных меток.

Фильтры можно легко перетаскивать между секциями, чтобы быстро менять логику отбора.

[Пример кнопки Умных меток в каталоге]
[Пример модального окна Умных меток]

Интеграция «Умных меток» в Помощник подарков

Что это? Та же самая система фильтрации по меткам доступна и в «Помощнике подарков» для вашего списка желаемого.

Почему это важно?

  • Теперь вы можете ещё точнее настраивать подборку игр из вашего вишлиста перед анализом цен или поиском подарка для друга.
  • Например, можно легко исключить все игры с меткой «Хоррор» или, наоборот, посмотреть только «Кооперативные рогалики», задав соответствующие активные фильтры.

Кнопка «Умные метки...» добавлена в раскрывающуюся панель фильтров.

[Пример Умных меток в Помощнике подарков]

Что нового в версии 2.1.4

Версия 2.1.4

В этой версии было добавлено несколько новых функций, улучшена работа существующих модулей и расширен список поддерживаемых магазинов.


Новые возможности

Улучшение внешних ссылок (ExternalLinksEnhancer)

Что это? Этот модуль автоматически добавляет на страницы игр в магазине и сообществе Steam недостающие кнопки со ссылками на полезные ресурсы.

Какие ссылки добавляются?

  • PCGamingWiki: Для быстрого доступа к технической информации, фиксам и настройкам игры.
  • SteamDB: Для просмотра подробной статистики, истории цен и информации о пакетах.
  • Страница в магазине: (Только на страницах сообщества) Добавляет кнопку для перехода в магазин, если она отсутствует.

Почему это важно? Модуль особенно полезен на страницах, открытых в «виртуальном режиме инкогнито», где другие расширения (например, Augmented Steam или официальное расширение SteamDB), добавляющие аналогичные кнопки, могут не работать. Кроме того, Steam часто скрывает кнопку перехода в магазин для игр, недоступных в вашем регионе, и этот модуль восстанавливает её, избавляя от необходимости искать обходные пути.

Исправление виджетов (WidgetFixer)

Что это? Фоновый модуль, который автоматически исправляет встроенные виджеты Steam (например, на форумах или в ленте активности), если они заблокированы в вашем регионе.

Как это работает? Скрипт обнаруживает заблокированный виджет и пытается перезагрузить его, используя регионы из настроек «Инкогнито». Теперь вы будете знать, что за игра скрывалась за надписью о недоступности, без необходимости изучать код страницы.

Почему это важно? Вместо сообщения об ошибке вы увидите полноценный виджет с информацией об игре, как если бы он не был заблокирован.


Улучшения и исправления

Агрегатор цен (%)
  • Добавлены новые магазины: В список поддерживаемых магазинов для сравнения цен были добавлены IGM.gg и Sous-Buy.ru.
  • Настройка для IGM.gg: Для магазина IGM.gg добавлена опция, позволяющая учитывать наличие подписки, чтобы цены отображались с учётом соответствующей скидки.
Помощник подарков

Правила отправки подарков были скорректированы для более точного соответствия механизмам Steam.

  • Порог разницы увеличен до 15%: Ранее лимит составлял 10%. Он был увеличен, чтобы учесть использование Steam разных и часто неизвестных курсов конвертации валют для разных сервисов (магазин, торговая площадка, магазин очков). Это позволяет дарить игры в большем количестве случаев, однако стоит помнить, что для некоторых валютных пар реальный лимит Steam может быть и ниже 15%.
  • Отменена проверка в "минус": Убрано правило, запрещавшее дарить игру, если цена в регионе дарителя значительно выше. Теперь проверка работает только в одну сторону: блокируется отправка подарка из дешёвого региона в дорогой, если разница в цене превышает установленный порог. Это соответствует политике Steam по борьбе со злоупотреблениями.
Общие улучшения
  • Включены по умолчанию: Новые модули «Улучшение внешних ссылок» и «Исправление виджетов» включены по умолчанию для всех пользователей.
Что нового в версии 2.1.3

Версия 2.1.3

Это техническое обновление, направленное на исправление критической ошибки в работе функции «Виртуальный режим „Инкогнито“». Значительно повышена надежность и совместимость, особенно для пользователей браузера Firefox.


Исправления

Виртуальный режим «Инкогнито»
  • Повышенная надежность получения сессии: Полностью переработан механизм получения анонимной сессии для обхода блокировок. Это исправляет критическую ошибку, из-за которой функция не работала у некоторых пользователей Firefox.
  • Универсальный парсинг: Новый метод использует регулярные выражения для прямого извлечения cookie, что делает его нечувствительным к форматированию заголовков ответа разными браузерами и расширениями.
Что нового в версии 2.1.2

Версия 2.1.2

Это обновление сфокусировано на повышении удобства и надежности. Добавлены новые настройки автоматизации для «Агрегатора цен», улучшен механизм обхода региональных блокировок и доработана логика отображения данных в «Помощнике подарков».


Улучшения и нововведения

Виртуальный режим «Инкогнито»
  • Настройки автоматизации: В главном меню настроек U.S.E. появились опции для автоматического запуска сбора цен при открытии окна и автоматической подстановки названия игры в фильтр после сбора данных.

⚠️ Важно:

При включенной авто-подстановке для более точного поиска рекомендуется сокращать название.
Например, вместо "DEATH STRANDING DIRECTOR'S CUT" лучше оставить "DEATH STRANDING".

  • Улучшенная обработка валют: Корректно обрабатываются и конвертируются цены в USD от магазинов SteamPay и GamerBase, что повышает точность в обоих валютных режимах (RUB/USD).
  • Округление цен: Цены в рублях теперь округляются до целых чисел для лучшего восприятия.

 

Виртуальный режим «Инкогнито»
  • Резервный механизм (Fallback): Если не удается обойти блокировку в регионе, указанном в настройках, скрипт автоматически попытается загрузить страницу через цепочку резервных регионов (США → Швейцария → Казахстан → Япония), пока не найдет рабочий вариант.
Доступность подарков (Список желаемого)
  • Улучшенное отображение рейтинга: Рейтинг теперь рассчитывается и отображается на основе процентного соотношения, а не только на основе официального ярлыка Steam. Это позволяет видеть оценку даже для игр с небольшим количеством обзоров, у которых ранее мог отсутствовать текстовый рейтинг.
Что нового в версии 2.1.1

Версия 2.1.1

Это обновление вносит ряд улучшений в существующий функционал.


Улучшения и нововведения

Настройки Виртуального режима «Инкогнито»
  • Отключение режима: В настройки скрипта добавлена возможность полностью отключать автоматический запуск «виртуального режима инкогнито».
  • Смена региона: Теперь можно выбрать регион по умолчанию (например, Турция или Аргентина вместо США), от имени которого будет запрашиваться страница.
  • Кнопка ручного запуска: Добавлена опциональная кнопка «in» рядом с логотипом Steam для принудительной перезагрузки любой страницы в «виртуальном режиме инкогнито». (Изначально отключена)
[Изображение: Настройки режима Инкогнито]
Агрегатор цен (%)
  • Добавлен магазин Rushbe: В список магазинов агрегатора цен добавлен официальный магазин Rushbe от МТС.
Поиск цен на Plati.Market
  • Импорт/Экспорт исключений: Добавлена возможность экспортировать и импортировать список слов-исключений, аналогично тому, как это реализовано в агрегаторе цен.
Доступность подарков (Список желаемого)
  • Расширенные карточки и фильтры: Модуль был значительно переработан.
    • В карточку игры добавлены новые данные: издатель, разработчик, серия игр, метки, статус раннего доступа и информация о поддержке русского языка.
    • Для пользователей из РФ добавлена информация о соответствии цены Рекомендованной Региональной Цене (РРЦ) от Valve.
    • Добавлена расширенная система фильтрации, позволяющая отбирать игры по цене, скидке, дате выхода, рейтингу, языку, статусу Раннего Доступа и соответствию РРЦ.
[Изображение: Обновленный помощник подарков]
Что нового в версии 2.1.0

Версия 2.1.0

Это обновление направлено на решение ключевых проблем, с которыми сталкивались пользователи, а также на улучшение стабильности и удобства использования скрипта.


Ключевое нововведение: Виртуальный режим инкогнито

Ранее для просмотра страниц, недоступных в регионе, пользователи прибегали к ручным методам: выходили из аккаунта, использовали другой браузер или открывали страницу в режиме инкогнито браузера (самый быстрый), после чего добавляли в адрес ?cc=us. Однако в режиме инкогнито этот способ имел критический недостаток - "age gate" (проверка возраста). Steam не позволял подтвердить возраст для "запрещенной" территории, делая метод бесполезным для игр с рейтингом 18+. Пользователи предлагали альтернативные решения — сделать простое перенаправление в центр сообщества, но это лишило бы пользователей ценной информации, предоставляемой нашим скриптом.

В связи с этим мы разработали "виртуальный режим инкогнито" — комплексное решение, которое обходит эти ограничения.

TGmRkOP.png
 

Как это работает?
  1. При заходе на заблокированную страницу скрипт распознает ошибку.
  2. В фоновом режиме запрашивается анонимная сессия у Steam (временные sessionid и browserid), имитируя визит нового, незалогиненного пользователя.
  3. С этими временными данными скрипт запрашивает американскую версию страницы (?cc=us) на русском языке (&l=russian).
  4. Если на странице есть проверка возраста, скрипт автоматически проходит ее, добавляя cookie с датой рождения.
  5. Содержимое заблокированной страницы полностью заменяется на полученный разблокированный HTML-код. Вы остаетесь на том же URL, но видите страницу так, как если бы зашли на неё с прописанным ?cc=us без входа в аккаунт.
  6. Все модули U.S.E. перезапускаются для корректной работы на новом коде страницы.

PpjV7oc.png

Преимущества:

  • Бесшовная работа: Больше не нужно открывать окна в режиме инкогнито и вручную менять URL.
  • Динамический контент: После загрузки страницы обзоры и другая динамическая информация подгружаются уже с вашего аккаунта, включая обзоры от друзей (которые невозможно получить при обычном просмотре без входа).

⚠️ Важно:

Наш подход не нарушает "Соглашение подписчика Steam", поскольку не использует VPN, прокси и другие запрещенные соглашением средства для сокрытия IP-адреса. Он задействует легитимный, предоставленный самой Valve механизм просмотра страниц для другого региона — параметр ?cc=. Запрос анонимной сессии необходим исключительно для того, чтобы изолировать этот просмотр от данных основного аккаунта. По сути, скрипт показывает то, что любой человек видит, зайдя на страницу игры с параметром ?cc=us без входа в аккаунт. Эта функция предназначена исключительно для просмотра информации и обеспечения работы функций расширения. Она не является инструментом для совершения нелегитимных покупок.


Улучшения и исправления

Совместимость и стабильность
  • Частичная поддержка AdGuard: Решена проблема с падением скрипта при использовании в AdGuard. Добавлена директива @grant unsafeWindow. Часть проблем может сохраняться из-за особенностей работы самого AdGuard, которые мы не можем обойти.
Помощник подарков и Анализатор цен
  • Улучшен сбор данных: Добавлен дополнительный метод определения валюты пользователя для более стабильной и точной работы модуля.
Агрегатор цен (%)
  • Конвертация в USD: В окно агрегатора добавлена кнопка "USD", позволяющая конвертировать все цены в доллары США.

DcidcTe.png

Время друзей
  • Работа в «виртуальном режиме инкогнито»: Добавлена возможность видеть информацию о друзьях (кто владеет игрой или добавил ее в желаемое) даже на страницах, загруженных с помощью нового механизма.

6jbZ03L.png

Что нового в версии 2.0 (для тех, кто в курсе)

Версия 2.0.0

Юбилейное обновление знаменует собой новую эру для Ultimate Steam Enhancer.


Глобальное улучшение: Меню настроек U.S.E.

Теперь все опции доступны через удобное графическое меню.

  • Доступ: Меню настроек U.S.E. находится в выпадающем меню вашего профиля Steam (в правом верхнем углу).
  • Управление модулями: Легко включайте и отключайте отдельные модули скрипта.
  • Дополнительные опции: Настраивайте поведение конкретных функций, таких как автоматическое раскрытие блоков или загрузка данных.
[Изображение: Расположение меню настроек U.S.E. в профиле Steam]
[Изображение: Пример панели настроек Ultimate Steam Enhancer]

Это изменение делает скрипт значительно более дружелюбным для пользователей и упрощает его персонализацию.


Новые функции

Анализатор цен (Региональные цены Steam)

Что делает: Добавляет кнопку "Анализатор цен" на страницу игры. Этот мощный инструмент позволяет детально анализировать региональные цены игры в Steam двумя способами: в рублях (по умолчанию) и в долларах США (переключаемый режим).

После нажатия кнопки "Сбор данных" в специальном окне, модуль выполняет следующее:

Режим Рублей:

  • Определяет AppID текущей игры и запрашивает цены через официальное API Steam для 41 региона.
  • В качестве базы для расчета рекомендованной рублевой цены используется цена в США (USD).
  • Цены из всех регионов, включая Россию, конвертируются в рубли по актуальным обменным курсам для прямого сопоставления.
  • Производится ключевое сравнение: фактическая цена в российском Steam сопоставляется с официально рекомендованной Valve ценой для России. Отклонения подсвечиваются.
  • Отображается рейтинг российской цены среди всех проанализированных стран, позволяя увидеть её место от самой дешёвой к самой дорогой в рублевом эквиваленте.

Режим Долларов США:

  • Активируется кнопкой "USD" в окне анализатора. Интерфейс и названия валют переключаются на английский язык.
  • Цены всех регионов также запрашиваются через API Steam и конвертируются в доллары США.
  • Цена в США используется как базовый ориентир (100%) для сравнения с ценами других регионов.
  • Отображается процентное отклонение цен других регионов от цены в США.
  • Представляется общий рейтинг всех региональных цен в долларовом эквиваленте.
  • Этот режим полезен для оценки ценовой политики при общении с разработчиками/издателями.

Примечание: Если игра в США бесплатна или цена для нее не найдена, возможности анализа в обоих режимах могут быть ограничены.

⚠️ Важная информация:

Каждый полный сбор данных подразумевает отправку ~41 запроса к серверам Steam. Пожалуйста, используйте эту функцию обдуманно. Частое использование может привести к временному ограничению доступа к API Steam.

[Изображение: Пример работы анализатора региональных цен]
Агрегатор цен (%)

Что делает: Заменяет старый модуль "Цены (VGT)". Добавляет кнопку "%" рядом с кнопкой "В желаемое" на странице игры. Нажатие открывает модальное окно с ценами на эту игру из различных популярных цифровых магазинов.

Возможности окна агрегатора:

  • Отображение предложений из множества магазинов, включая: SteamBuy, Playo, SteamPay, Gabestore, GamersBase, Igromagaz, GamesForFarm, Gamazavr, GameRay, KupiKod, KeysForGamers, Zaka-zaka, Buka, GGSEL, Plati.Market и текущей страницы Steam.
  • Сортировка по цене, проценту скидки, сумме скидки, названию.
  • Фильтрация по диапазону цен, проценту и сумме скидки, наличию скидки, названию (слова через ";"), магазинам.
  • Исключение товаров по ключевым словам.
  • Сохранение состояния фильтров, сортировки и исключений между сессиями.
  • Экспорт и импорт списка исключений для удобного переноса и резервного копирования.
Список рекомендуемых исключений

Нажмите кнопку импорта () и вставьте в появившееся окно следующий текст:





онлайн,оффлайн,dlc,аккаунт,ps4,xbox,digi361,ibloodrue,mangarded,siparisapp,multimarket,trustytop,cyber steam shop,boostgame,cloud-gaming.store,promarket88,offline,-seyter-,paul fox,sirdjinn,аренда,4ilgames,keks12,damhubmarket,nt-store,ps5,@mediasoft,dofamine game,hactier,alz0n,top-games,solo29,nerd shop,1gamestore,tobeclosertoyou

Использует различные методы для получения цен (API, парсинг HTML) для предоставления наиболее полной картины.

[Изображение: Пример интерфейса агрегатора цен]
Доступность подарков (на странице игры)

Что делает: Добавляет кнопку "GIFT" в блок с кнопкой "В желаемое" на странице игры.

Нажатие открывает окно, где можно:

  • Выбрать регион друга из списка.
  • Нажать "Узнать", чтобы запросить цену игры в этом регионе.
  • Увидеть цену друга (сконвертированную в вашу валюту), процент разницы и вердикт (Можно подарить / Нельзя подарить), основанный на правилах Steam о разнице цен (обычно ±10-15%).

Использует те же механизмы получения цен и курсов валют, что и помощник для списка желаемого.

[Изображение: Пример работы помощника подарков на странице игры]
Рулетка Stelicas (Лента активности)

Что делает: Добавляет блок "Рулетка Stelicas" на страницу вашей активности Steam (steamcommunity.com/my/home). Позволяет загрузить CSV-файл, сгенерированный приложением Stelicas, применить к нему разнообразные фильтры и случайным образом выбрать игру из вашей коллекции.

Возможности:

  • Загрузка CSV-данных из Stelicas.
  • Система фильтрации: по категориям коллекций Stelicas, дате выхода, пользовательским тегам, поддержке русского языка (интерфейс, субтитры, озвучка), а также по диапазонам количества отзывов и общего рейтинга игры.
  • Анимированная рулетка для выбора случайной игры.
  • Возможность включить приоритет по отзывам и рейтингу.
  • Переключение в режим просмотра всей отфильтрованной подборки игр.
  • Отображение подробной информации о выбранной игре.
  • Прямые ссылки на страницу игры в Steam и для её запуска.

Подробная инструкция доступна по нажатию на значок вопроса ? в окне самой рулетки.

[Изображение: Пример модального окна Рулетки Stelicas с фильтрами]
Доступность подарков (Список желаемого)

Что делает: Добавляет значок лупы на страницу списка желаемого. Позволяет определить, какие игры из вашего вишлиста можно подарить друзьям в других регионах.

Основные функции:

  • Загружает игры из отображаемого списка желаемого и выводит их в виде информативных карточек с возможностью сортировки.
  • Активирует режим помощника подарков:
    • Вы выбираете регион вашего друга.
    • Скрипт запрашивает цены на игры для выбранного региона.
    • Цены друга конвертируются в вашу валюту.
    • Отображается разница в цене с цветовой индикацией (зелёный - можно дарить, красный - нельзя).
    • Доступен фильтр "Можно подарить".

Это помогает легко найти подходящие и экономически целесообразные подарки для друзей за границей.

[Изображение: Пример работы помощника подарков для списка желаемого]

Улучшения существующих функций

Индикаторы / Доп. обзоры / Монитор обзоров

Модуль информации об обзорах на странице игры был расширен:

  • Новинка: Глобальный монитор обзоров!
    • При щелчке по строке "Тотальные" открывается окно "Глобальный монитор обзоров".
    • Этот инструмент позволяет собрать (по кнопке "Собрать") и визуализировать статистику обзоров по 27 языкам Steam.
    • Отображается таблица с рейтингом языков по количеству обзоров и доле в общей массе.
    • Интерактивная круговая диаграмма для наглядного представления распределения обзоров по языкам.
[Изображение: Глобальный монитор обзоров с таблицей и диаграммой]

⚠️ Важное замечание о зависимостях:

Отключение этого модуля через новое меню настроек приведет к автоматическому отключению или нарушению корректной работы модулей «Время прохождения (HLTB)», «Русификаторы (ZOG)» и «Время друзей / Глобальные достижения», так как они критически зависят от его функционала по отображению элементов на странице игры.

Наблюдатель (Желаемое/Библиотека)

Модуль отслеживания был полностью переработан, получил новое имя "Наблюдатель" и значительно расширил свои возможности. Теперь он следит не только за списком желаемого, но и за вашей библиотекой, а также предлагает гибкие настройки для ускорения работы.

  • Комплексное отслеживание: "Наблюдатель" теперь отслеживает изменения как для Списка желаемого, так и для Библиотеки:
    • Для желаемого: изменение даты выхода, выход из раннего доступа, появление/изменение русского языка.
    • Для библиотеки: выход игры из раннего доступа, а также появление или изменение русской локализации (с точным указанием, что было добавлено или убрано: интерфейс, озвучка, субтитры).
  • Гибкие настройки сканирования: В панели уведомлений появился значок настроек, который открывает меню с новыми опциями:
    • Возможность включать или отключать проверку для желаемого и библиотеки по отдельности.
    • Оптимизация проверки библиотеки: Добавлены опции для значительного ускорения повторных сканирований. Можно отключить перепроверку игр, у которых уже есть любая русская локализация, или, для более детального контроля, перепроверять только те игры, у которых нет полной локализации.
  • Улучшенный интерфейс:
    • Раздельные индикаторы статуса (Ж/Б), показывающие, как давно обновлялись данные для Желаемого и Библиотеки.
    • Более информативные уведомления, четко разделенные по источнику (Желаемое/Библиотека) и типу изменения.
  • Управление хранилищем: Новая кнопка "Хранилище" позволяет вручную очистить кэш сохраненных данных для списка желаемого или библиотеки. Это полезно, если вы хотите принудительно запустить полное повторное сканирование.
     
[Изображение: Пример панели уведомлений Наблюдателя]
 
[Изображение: Пример окна управления хранилищем Наблюдателя]
Фильтр новостей

Модуль скрытия новостей в новостном центре получил следующие улучшения:

  • "Мягкое" скрытие: При установке галочки новость сначала становится полупрозрачной, отмечаясь для последующего подтверждения скрытия.
  • Панель управления:
    • Кнопка "Скрыть выбранные (X)" с счетчиком.
    • Индикатор "В хранилище: X" показывает общее количество скрытых новостей.
    • Кнопка "Отменить" для отмены последнего действия по скрытию (активна 6 секунд).
    • Переключатель "Показать скрытое" / "Спрятать скрытое" для временного отображения скрытых новостей в затемненном виде.
  • Новинка: Панель "Хранилище скрытых новостей"!
    • Открывается кнопкой "Хранилище" из панели управления.
    • Отображает список всех постоянно скрытых новостей (название игры, заголовок, AppID).
    • Позволяет выборочно "Вернуть" новость из хранилища.
    • Кнопка "Очистить хранилище" для полного удаления всех скрытых новостей (требует подтверждения).
[Изображение: Пример интерфейса фильтра новостей]
[Изображение: Пример интерфейса хранилища новостей]
Русификаторы (ZOG)

Модуль поиска русификаторов был кардинально переработан и больше не использует устаревающую базу данных.

  • Поиск в реальном времени: Вместо статической базы данных теперь скрипт выполняет поиск напрямую на сайте ZoneOfGames.ru в момент вашего запроса, гарантируя максимальную актуальность информации.
  • Выбор из совпадений: Если найдено несколько похожих игр, скрипт предложит вам список для выбора. Это исключает ошибки при поиске игр с похожими названиями (например, 'Half-Life' и 'Half-Life 2') и обеспечивает более высокую точность.
[Изображение: Пример отображения информации о русификаторах с ZOG]
Доп. инфо в ленте активности

Модуль, отображающий информацию об играх в ленте активности, был улучшен для более точного отображения изображений.

  • Точные изображения: Вместо использования стандартного имени файла header.jpg, скрипт теперь запрашивает у API Steam точное имя файла обложки игры. Это решает проблему, когда для некоторых игр отображалась неверная или отсутствующая картинка.
  • Повышенная надежность: Новый метод гарантирует, что во всплывающей подсказке будет показано именно то изображение, которое разработчик установил в качестве основного для страницы в магазине.

Технические изменения

  • Новые библиотеки:
    • Добавлена библиотека Chart.js для построения диаграмм в "Глобальном мониторе обзоров".
    • Добавлен плагин chartjs-plugin-datalabels для отображения меток данных на диаграммах.
  • Обновлены директивы @connect:
    • Добавлено множество новых доменов для работы "Агрегатора цен (%)", "Анализатора цен" и "Помощников подарков".
    • Удален домен vgtimes.ru в связи с заменой соответствующего модуля.
  • Общая оптимизация кода и улучшение стабильности работы существующих модулей.

Удаленные функции

  • Агрегатор цен (VGTimes): Модуль был полностью удален и заменен новым, более функциональным модулем "Агрегатор цен (%)".
  • Отслеживание вишлиста (wishlistTracker): Старый модуль был заменен на новый, более комплексный модуль "Наблюдатель (Желаемое/Библиотека)".
Скрытый текст

Версия 1.9.4 - Подсветка и фильтрация DLC для ваших игр

  • Основные изменения
    • Подсветка DLC для игр в вашей библиотеке
      • В каталоге поиска теперь подсвечиваются DLC для игр, которые уже есть в вашей библиотеке Steam (фиолетовый фон).
    • Новый фильтр "Только ваши DLC"
      • Добавлена возможность фильтрации, чтобы показывать только DLC для игр из вашей библиотеки.

MqjuXoD.png

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

Версия 1.9.3 - Хотфикс индикатора раннего доступа

Исправлено

  1. Отображение индикатора поверх других элементов из-за слишком высокого z-индекса
Скрытый текст

Версия 1.9.2 - Улучшение индикатора раннего доступа

Основные улучшения

  1. Расширенная база данных дат: 
    • Интеграция внешнего источника данных для определения начала раннего доступа, даже если дата не указана на странице игры.
    • Автоматическое кэширование данных на 6 месяцев для оптимизации производительности. 
      • Для активного раннего доступа: время с момента запуска до текущей даты.
      • Для вышедших из раннего доступа игр: период от раннего доступа до официального выхода.
  2. Улучшенная обработка сценариев
    • Отображение "срок неизвестен", если дата выхода в ранний доступ больше, чем дат выхода из раннего доступа.

Данное обновление связано с написанием блога “Сколько лет, сколько зим: Хроники раннего доступа”, в рамках разбора которого обнаружилось, что у нескольких тысяч игр отсутствуют метаданные о нахождении в раннем доступе, из-за чего скрипт, опирающийся на базы данных Steam, не выводил индикатор у этих игр. В связи с этим был проведён сбор дополнительной информации, после чего все обнаруженные игры были вынесены во внешнюю базу, по которой скрипт проверяет игры. 

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

Версия 1.9.1 - Индикатор раннего доступа

Новые функции

  1. Динамический расчет продолжительности раннего доступа: 
    • На страницу игр добавлен новый блок с динамическим расчетом продолжительности нахождения игры в раннем доступе: 
      • Для активного раннего доступа: время с момента запуска до текущей даты.
      • Для вышедших из раннего доступа игр: период от раннего доступа до официального выхода.

6iGlcTf.png

SPzJrpi.png

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

Версия 1.9 - Агрегатор цен

Новые функции

  1. Добавлен под-скрипт "Агрегатор цен” 
    • На странице игры отображается кнопка "Цены (VGT)" (находится в блоке с "в желаемое", "подписаться" и "скрыть") для отображения цен из магазинов.
    • Группировкой предложений по магазинам
      • Группировкой предложений по магазинам
      • Сортировкой магазинов по минимальной цене
      • Возможностью загрузить последующие результаты (пагинация по 40 позиций)
    • Реализована система распознавания игр через:
      • Прямое совпадение Steam AppID
      • Нормализацию названий и алгоритм нечёткого поиска
      • Ручной выбор из списка возможных совпадений  
    • Система использует актуальную базу данных VGTimes на 11.02.2024  (поэтому игры, вышедшие, после этой даты могут не находиться)

Изначально игры выводятся по релятивности, т.е. сочетание цены и соответствия запросу, т.к. иногда в агрегаторе в цены игры могут попадать цены на DLC и другие избыточные вещи (сейчас они обычно оказываются на последующих страницах.

Важно!

  • Если скрипт не может найти игру в базе VGT по Steam AppId, то он начинает искать по имени в довольно обширной базе.
  • Если идеальное совпадение имени обнаружено, то поиск займёт пару секунд.
  • Если идеальное совпадение не обнаружится, то скрипт соберёт все игры, названия которых как-то совпадают, это уже дольше.
  • Если скрипт затрудняется найти что-то похожее - поиск займёт ещё дольше. В таком случае страница может подвиснуть на 5-10 секунд).
Скрытый текст

7cJzB8a.png

fNRnaHw.png

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

Обновил Ultimate Steam Enhancer.

Версия 1.8 - Время друзей и достижения

Новые функции

  1. Добавлен под-скрипт "Время друзей & Достижения” 
    • Новый информационный блок на страницах игр с аналитикой времени игры друзей.
    • Отображение максимального, среднего и минимального времени прохождения среди друзей.
    • Интеграция статистики глобальных достижений с расчётом платины и среднего прогресса.

9TaMCbZ.png

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

Версия 1.7.4 -  Информация о метках в подсказках

Новые функции

  1. Добавлено отображение Steam-меток в подсказках 
    • Реализована интеграция с внешней базой русских названий тегов через GitHub Gist.
    • Всплывающие подсказки показывают до 5 основных тегов игры (Т.к. Steam нередко ограничивает их до трёх и меньше в поиске по каталогу).
    • Добавлена система кэширования тегов на 31 день для уменьшения количества запросов.

U7DYIvJ.png

xE75iU8.png

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

Версия 1.7.2 - Хотфикс

  1. Исправлен конфликт тултипов при включенной функции отслеживания вишлиста:
    • Переработана система именования CSS-классов (Добавлен уникальный префикс `wt-` к критическим элементам). Переименованы:  
      • `.tooltip` → `.wt-tooltip`
      • `.notification-item` → `.wt-notification-item`
      • `.panel-header` → `.wt-panel-header`
  • Исправлены селекторы в JavaScript для новых имен классов

0HzovQo.png

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

Версия 1.7.1 - Исправления

Исправленные проблемы:

  1. Некорректное позиционирование панели в виджетах
    • Добавлена проверка на основной документ перед вставкой элементов
    • Исключено встраивание в iframe и вложенные body

    eJi8L99.png

     

  2. Дублирующее открытие ссылок в календаре

    • Удален лишний обработчик щелчка для игр
    • Оставлена только нативная обработка ссылок через тег <a>
  3. Взаимодействие с фоном при открытом календаре

    • Модальное окно теперь закрывается при щелчке вне его области
    • Блокировка нежелательных действий с элементами страницы под модалкой
    • Добавлена защита от случайных переходов по ссылкам при закрытии

Технические изменения:

  • Использование нативного DOM API для добавления элементов
  • Оптимизировано определение контекста выполнения скрипта
  • Улучшена изоляция стилей через проверку области видимости
  • Добавлена глобальная обработка щелчков для модальных окон

Все прошлые исправления загоню в changelog в посте со скриптом.

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

Обновил Ultimate Steam Enhancer.

Версия 1.7 — Добавлен календарь релизов из списка желаемого

  • Модальное окно с помесячной разбивкой
  • Интерактивная сетка дней с предстоящими релизами
  • Визуальная индикация игр с точными/приблизительными датами
  • Поддержка разных форматов дат (квартал, год, месяц)
  • Динамическая подгрузка месяцев (пагинация по 3 месяца)
  • Подсказки с детализацией для приблизительных дат
  • Прямые ссылки на страницы игр из календаря

Bv5Be9Y.png

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

Когда HLTB отключался, ZOG терял референсную точку и падал в позицию (0,0), так как не было фолбэка на другие DOM-элементы. По сути, ZOG-блок вел себя как поезд, перед которым убрали рельсы.
 

Ключевые изменения:

  • Внедрил многоуровневую систему привязки позиции ZOG:
    • Первичная привязка к HLTB через ResizeObserver (если активен)
    • Фолбэк на блок русификаторов russianIndicators
    • Крайний случай - жесткая фиксация относительно gameHeaderImageCtn
  • Улучшил обработку мутаций через комбинацию Observers:
    
    
    const generalObserver = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
          updatePosition(); // Реакция на изменение структуры
          initObservers();  // Реинициализация при динамических изменениях
        }
      });
    });

     

Если вкратце:
Теперь, независимо от состояния gamePage/hltbData/zogInfo,  HLTB и ZOG должны правильно позиционироваться. 

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

Появился новый функционал:

9. Отслеживание изменений дат релиза по вашему списку желаемого Steam:

  • В правом верхнем углу страниц Steam появляется кнопка "Отслеживание вишлиста" со счетчиком непрочитанных уведомлений и индикатором статуса актуальности данных:
    • ОК (До 24 ч.)
    • ОК? (До 48 ч.)
    • ! (До 72 ч.)
    • !! (До 96 ч.)
    • !!! (Более 96 ч.)
    • ??? (Критическое устаревание, ошибка или отсутствие данных [при первом запуске]).
      • (При наведении курсора на индикатор вы увидите точные цифры).
  • При нажатии на кнопку выплывает интерфейс, где есть отдельная кнопка “Обновить” , с помощью которых осуществляется запрос актуальных данных через Steam API.
  • При изменении даты релиза игры в панели появляется карточка с:
    • Изображением и названием игры (кликабельная ссылка на страницу).
    • Старой и новой датами релиза.
    • Временем обнаружения изменения.
    • Кнопками для отметки прочтения и удаления каждого уведомления.
      • (Также присутствует кнопка очистки уведомлений).
         
  • Скрипт поддерживает все форматы: точные (например, "15.04.2025") и относительные (Coming Soon, TBA, кварталы, месяцы, годы) даты.
  • Скрипт получает список appID из вашей Userdata, которая доступна только вам. 
  • Запросы выполняются батчами по 200 appID к IStoreBrowseService/GetItems.
     
  • Важно!
    • Поскольку скрипт работает с userdata, он не будет работать, если вы не находитесь в своём аккаунте.
    • Обработка гигантских списков желаемого может занять время (делите размер списка желаемого на 200. 10000 игр = 50 запросов).
    • В скрипте установлено ограничение в сохранение 5000 уведомлений. Но сомневаюсь, что кому-то понадобится больше.
Скрытый текст

В части поиска по имени при получении информация о наличии переводов на ZOG:

  • Теперь алгоритм нормализации названий стал жестче:
    • Нормализует Unicode (NFD -> удаляет диакритику)
    • Выпиливает торговые марки (™®©) и стандартизирует апострофы
    • Фильтрует edition-based суффиксы через regexp: /ultimate|definitive|edition/i и т.д.
    • Конвертирует римские цифры в арабские (VII -> 7) хардкодным маппингом
    • Удаляет артикли (the/a/an) из начала строки
       
  • В findPossibleMatches появились триггеры contains помимо startsWith, плюс добавилась композитная сортировка:
    • сначала strict prefix match,
    • потом substring inclusion,
    • потом уже по проценту схожести через алгоритм Левенштейна.
  • Теперь обработка названия игры идет через пайплайн:
    • rawName → normalizeTitle → removeEditionWords → processRomanNumerals → processArticles
       
  • Для Steam API-ответов добавился recursive processing — если после очистки английского названия не находится матч, идет обращение к possibleMatches с модифицированными levenshteinDistance thresholds.
     
  • Теперь должно лучше детектить игры с разными изданиями и переводами названий.
Скрытый текст

Улучшены:

  • Информация о времени прохождения игры с HLTB
  • Информация о наличии переводов на ZOG (ZoneOfGames) 

Поиск по имени является основным методом нахождения игры на HLTB и вспомогательным для поиска игры на ZOG.
Данный метод был улучшен, теперь:

  • Он осуществляет более тщательный поиск с учётом возможных опечаток, укорачиваний и удлинений названий.
  • Предоставляет возможность выбрать, какое из похожих названий подходит вам больше всего
Скрытый текст

Появился функционал на странице игры:

3. Информация о наличии переводов на ZOG (ZoneOfGames):

  • На странице игры отображается блок с информацией о наличии русификаторов (и не только) на ZoneOfGames. Включает:
    • Название игры [и ссылку на неё в базе ZOG].
    • Информацию о наличии перевода (языке, типе, размере) или его отсутствии [со ссылками на на него в базе ZOG] 
       
  • Важно!
    • Скрипт опирается на базу, собранную 05.02.2025. Поэтому информация в нём актуальна на этот день. Если я обновлю базу — скрипт автоматически её подтянет.
    • Скрипт ищет информацию базе двумя способами:
      • По App ID игры.
        (Если в базе ZOG указан Steam App ID игры, то вы увидите, есть ли на неё перевод.)
      • По названию игры.
        (Если в базе ZOG содержится название игры из Steam).
    • Оба способа не гарантируют 100% получение информации о наличии переводов, поскольку:
      • Не у всех игр в базе указан Steam App ID.
      • Не все названия в базе совпадают с названиями игр в Steam. 
Скрытый текст

Переработал логику в фильтрах по русскому переводу:

uMoyAmL.png

Теперь работает так:

  • Если вы нажимаете “только текст”, то показывает игры с переводом субтитров/интерфейса, но без озвучки.
  • Не изменилось:
    • Если вы нажимаете озвучка — показывает все игры с озвучкой.
    • Если вы нажимаете без перевода — показывает игры без перевода.
Скрытый текст

Дополнился функционал при поиске по каталогу:

3. Дополнительная информация об играх и фильтрация по русскому языку:

  • Обновление:
    • В каталоге поиска справа появилось меню “Русский перевод”:
      • Русский перевод (оставляет игры с русским интерфейсом/озвучкой/субтитрами)
      • Русская озвучка (оставляет игры, где обязательно есть русская озвучка)
      • Без русского (оставляет игры, где нет русского)
Скрытый текст

Добавил седьмую функцию.

7. Дополнительная информация при наведении на игру в ленте вашей активности Steam:

  • При наведении мыши слева появляется всплывающая подсказка с детальной информацией о игре.
  • Включает: название, изображение, дату выхода, издателей, разработчиков, серию игр, отзывы, информацию о раннем доступе, поддержку русского [и английского] языка и краткое описание.

 

 

 

 

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

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


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

SteamDB - Sales; Ultimate Enhancer || Tamper Monkey

SteamDB - Sales; Ultimate Enhancer - это расширенный пользовательский скрипт для раздела скидок на SteamDB, который добавляет множество полезных функций для более комфортного использования платформы. Скрипт объединяет в себе функционал нескольких популярных инструментов, предоставляя пользователю расширенные возможности для работы с играми, фильтрами и конвертацией валют.

Скрипт соединяет в себе функционал из:

Подробное описание, официальный функционал и скриншоты

Официальное описание функционала

1. Расширенные фильтры
  • Русский перевод:
    • Фильтрация по наличию только текстового перевода (интерфейс/субтитры).
    • Фильтрация игр по наличию русской озвучки.
    • Фильтрация игр без русского перевода.
  • Списки (подходят для просмотра недоступных в России игр):
    • Сохранение кастомных списков игр (Список 1/Список 2).
    • Фильтрация уникального по пересечению списков.
  • Фильтр по дате начала распродажи (календарь).
  • Фильтры по обзорам и раннему доступу:
    • Фильтрация в реальном времени по количеству обзоров (минимальное/максимальное).
    • Фильтрация в реальном времени по проценту положительных обзоров (минимальный/максимальный).
    • Фильтрация по статусу раннего доступа (показать/скрыть).
  • Анализ РРЦ (Рекомендованной Региональной Цены) для РФ:
    • При выборе российской валюты (RUB) на SteamDB, скрипт позволяет анализировать цены игр относительно рекомендованных Steam цен для российского региона.
    • Слева от названия игры отображается индикатор:
      • < РРЦ (синий): Текущая полная цена игры в рублях ниже рекомендованной Steam. Отображается процент и сумма разницы.
      • = РРЦ (зеленый): Текущая полная цена игры в рублях соответствует рекомендованной Steam.
      • > РРЦ (красный): Текущая полная цена игры в рублях выше рекомендованной Steam. Отображается процент и сумма разницы.
      • Нет данных РРЦ: Если не удалось получить данные о цене в USD для расчета.
    • Добавлены кнопки для фильтрации игр по этому критерию: "< РРЦ", "= РРЦ", "> РРЦ".
  • Фильтры по типу скидок и проценту от исторического минимума (ATL):
    Стандартный блок фильтров скидок SteamDB заменен на более продвинутый:
    • Фильтры по типу исторической цены: Позволяют отображать или скрывать игры на основе цвета их скидки на SteamDB (синий - новый ист. минимум, зеленый - повтор мин. цены, фиолетовый - мин. за 2 года).
    • Фильтры по процентам в историческом минимуме (ATL): Сравнивают текущий процент скидки с процентом скидки, который был при достижении игрой All-Time Low цены. Позволяют отфильтровать игры, которые сейчас выгоднее/так же/менее выгодны по проценту скидки, чем были при своем ATL.
      • % < Минимума: Текущий % скидки > Исторического % ATL (выгоднее).
      • % = Минимуму: Текущий % скидки = Историческому % ATL.
      • % > Минимума: Текущий % скидки < Исторического % ATL (менее выгодно по % скидки).
2. "Тотальная" сортировка
  • Активируемая опция, которая изменяет стандартную сортировку по рейтингу на SteamDB.
  • Вместо простого процента положительных отзывов, используется формула: (количество обзоров * процент рейтинга).
  • Это позволяет эффективнее находить популярные и одновременно высокооцененные игры, поднимая в топ списка проекты, которые имеют и высокий рейтинг, и большое количество отзывов.
2. Конвертер валют
  • Возможность ручной настройки курса (по умолчанию: 1 = 0.19, задано для перевода тенге в рубли; можно вбить любой курс, в зависимости от используемой вами валюты).
3. Расширенная информация
  • При наведении на игру отображается всплывающая подсказка с:
    • Серией игр (франшизой).
    • Процентом положительных отзывов.
    • Статусом Early Access (ранний доступ).
    • Подробной информацией о языковой поддержке (русский [и английский, если включено в настройках]).
    • Кратким описанием игры.
  • Индикатор РРЦ (РФ): Как описано выше, слева от названия каждой игры (при активной российской валюте) отображается информация о соответствии цены рекомендованной Steam.
  • Визуализация процента скидки ATL: Текст "at -X%" в информации об All-Time Low подсвечивается цветом, показывая, насколько текущий процент скидки выгоден по сравнению с историческим процентом ATL.
    • Синий: Текущий % скидки > Исторического % ATL.
    • Зеленый: Текущий % скидки = Историческому % ATL.
    • Фиолетовый: Текущий % скидки < Исторического % ATL.
4. Калькулятор желаемого
  • Активируется кнопкой "Высчитать" на панели скрипта, если на SteamDB выбран фильтр "Your wishlist" в "Filter by type".
  • Собирает данные о ценах для игр из вашего списка желаемого.
  • Отображает в модальном окне таблицу с AppID, названием, текущей скидкой, текущей ценой, ~полной ценой, All-time Low, 2-year Low и лучшей ценой для расчета.
  • Позволяет сортировать таблицу по любому столбцу.
  • Рассчитывает и показывает итоговые суммы для покупки всех игр по ~полным ценам и по лучшим доступным ценам.

Скриншоты

Пример: Игры, доступные в Казахстане и недоступные в России, с русским текстовым переводом, скидки на которые появились не раньше 19.06.2025

5OQm5SW.png

Пример: Игры, доступные в русском регионе, цена на которые ниже рекомендуемых региональных Valve цен, с русской озвучкой, вне раннего доступа, с рейтингом выше 90%

zTuDI1q.png

Пример: Интерфейс калькулятора желаемого.

s53CiaE.png

Инструкция по установке:

  1. Установите расширение для пользовательских скриптов (например, Tampermonkey или Greasemonkey)
  2. Создайте новый пользовательский скрипт и вставьте код

Код скрипта на GreasyFork

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

 

 

 

 

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

Версия 1.3

  • Автоматический выбор "All entries":
    • Скрипт теперь автоматически выбирает "All (slow)" в выпадающем списке количества записей на странице и ожидает загрузки перед началом обработки. Больше не нужно делать это вручную. Появится таймер обратного отсчета, информирующий о процессе.
  • Калькулятор желаемого:
    • Добавлена новая кнопка "Высчитать" в панели скрипта, когда активен фильтр "Your wishlist" в "Filter by type" на SteamDB.
    • Эта функция собирает данные о ценах (текущая, исторический минимум, минимум за 2 года) для игр из вашего списка желаемого и отображает их в таблице в модальном окне.
    • Таблица включает: AppID, Название, Текущую скидку, Текущую цену, ~Полную цену (расчетную), All-time Low, 2-year Low и Цену для расчета (лучшую из доступных).
    • Поддерживается сортировка по любому столбцу.
    • Также отображаются итоговые суммы: если купить все по ~полным ценам и если купить все по лучшим доступным ценам.
  • Расширенные фильтры по скидкам (заменяют стандартные фильтры SteamDB):
    Скрытый текст

    Стандартный блок фильтров скидок на SteamDB был заменен на более продвинутый, предоставляемый скриптом. Он включает две категории:

    1. Фильтры по типу исторической цены (абсолютной):
      • Ист. минимум (синий цвет скидки на SteamDB): Показать/Скрыть игры, цена на которые является новым историческим минимумом.
      • Повтор мин. цены (зеленый цвет скидки): Показать/Скрыть игры, цена на которые повторяет предыдущий исторический минимум.
      • Мин. за 2 года (фиолетовый цвет скидки): Показать/Скрыть игры, цена на которые является минимальной за последние два года (но не историческим минимумом).
    2. Фильтры по процентам в историческом минимуме (ATL):
      Эта группа фильтров анализирует соотношение текущего процента скидки игры и процента скидки, который был у нее при достижении All-Time Low (ATL) цены.
      • % < Минимума (синий): Показать/Скрыть игры, где текущий процент скидки выше, чем процент скидки при ATL (т.е. игра сейчас выгоднее, чем была при ATL по проценту).
      • % = Минимуму (зеленый): Показать/Скрыть игры, где текущий процент скидки равен проценту скидки при ATL.
      • % > Минимума (фиолетовый): Показать/Скрыть игры, где текущий процент скидки ниже, чем процент скидки при ATL (т.е. игра сейчас менее выгодна по проценту, чем была при ATL, даже если абсолютная цена ATL такая же или ниже).

    Для каждого фильтра есть две опции: "Показать" (оставляет только соответствующие игры) и "Скрыть" (исключает их). Если активен фильтр "Показать" для какого-либо типа, то соответствующий фильтр "Скрыть" автоматически деактивируется, и наоборот.

     

  • Визуализация процента скидки ATL:
    • Текст вида "at -X%" в дополнительной информации под названием игры (где указан All-Time Low) теперь подсвечивается цветом в зависимости от того, как текущий процент скидки соотносится с этим историческим процентом скидки:
      • Синий:Текущий % скидки > Исторического % ATL (выгоднее).
      • Зеленый: Текущий % скидки = Историческому % ATL.
      • Фиолетовый: Текущий % скидки < Исторического % ATL (менее выгодно по % скидки).
  • Обновлен интерфейс панели управления скрипта:
    • Улучшено расположение элементов.
    • Обновлены тексты статусов и кнопок для лучшего информирования пользователя.
  • Изменение цветовой схемы для индикатора РРЦ:
    • = РРЦ (соответствует) теперь зеленый (ранее был синий).
    • < РРЦ (дешевле) теперь синий (ранее был зеленый).
    • > РРЦ (дороже) остался красным.
Скрытый текст

Игры, доступные в Казахстане и недоступные в России, с русским текстовым переводом, скидки на которые появились не раньше 02.06.2025

BNZCExN.png

 


Игры, доступные в русском регионе, цена на которые ниже рекомендуемых региональных Valve цен, с русской озвучкой и ценой, которая соответствует прошлому историческому минимуму

hcu3kck.png

 


Интерфейс калькулятора желаемого

s53CiaE.png

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

Версия 1.2 - Анализ РРЦ

  • Новая функция: Анализ РРЦ (Рекомендованной Региональной Цены) для РФ.
    • Предыдущая система "Рангов цен (RU)", основанная на статичной базе данных из Gist, была удалена. Статичная база быстро устаревала и требовала ручного обновления, что делало ее неэффективной.
    • Взамен добавлена динамическая система анализа РРЦ. Если на SteamDB выбрана российская валюта (RUB), скрипт теперь:
      1. При нажатии "Обработать игры" получает начальные (полные, без скидок) цены игр как для российского региона (в рублях), так и для американского региона (в долларах США) через Steam API.
      2. Рассчитывает рекомендованную цену в рублях на основе цены в долларах США, используя официальную сетку ценообразования Steam.
      3. Отображает слева от названия игры индикатор, показывающий, ниже ли текущая рублевая цена, равна или выше рекомендованной (< РРЦ, = РРЦ, > РРЦ), а также разницу в процентах и рублях.
      4. Позволяет фильтровать игры по этим трем категориям РРЦ.
    • Этот функционал активен только при выборе российской валюты на SteamDB. Для других валют скрипт работает в прежнем режиме (без анализа РРЦ).

ngx3juW.png

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

Версия 1.1 - Ранги и выгода для РФ

  • Главное нововведение:

    • Интеграция данных о выгодности цен в российском регионе Steam
      (На основе данных исследования, собранных 3 апреля 2025 года для ~80 тыс. игр)

      • Новый фильтр "Ранги цен (RU):" Позволяет легко отфильтровать игры по их ценовому рангу в России (например, показать только игры, где цена самая низкая в мире (ранг 1) или входит в топ-N).

      • Визуальный индикатор: Слева от каждой игры теперь отображаются 5 ключевых показателей:
                1.  Ранг цены РФ (1-39).
                2.  % разница с ближайшим конкурентом.
                3.  Руб. разница с ближайшим конкурентом.
                4.  % разница со средней ценой.
                5.  Руб. разница со средней ценой.

        • Для игр, вышедших после 3 апреля 2025, будет указано "Нет данных".

      • Доп. информация:
        • Данные о рангах подгружаются с Gist и кэшируются для быстрой загрузки. Добавлен индикатор статуса загрузки этих данных.

g9LG5JT.jpeg

 

 

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

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


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

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

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


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

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

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

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

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

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

Войти

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

Войти сейчас


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

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

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

    • @Amigaser я видел что ты писал, но может все таки есть какой нибудь способ наладить текст?   первое что мне пришло в голову это поиграться со шрифтом у него куча параметров для редактирования
    • @allodernat да мне нужно достать файлик чтобы он был полный русский а из за расхождения pathid он битый получается вот и мучаюсь
    • наткнулся на возможно годную в будущем игрушку, трейлер смотрится довольно мясисто так что добавил в желаемое. стим страница https://store.steampowered.com/app/1551980/Clive_Barkers_Hellraiser_Revival/  трейлер https://www.youtube.com/watch?v=R88QdlIlg-E
    • @Chillstream оригинал хочешь вытащить?
    • @stevengerard я вас не оскорблял. Вы меня с кем-то путаете. Можете ставить минусы к моим постам, мне по-барабану. Чего вы такой злой? У вас какие-то проблемы на личном фронте? Я эту игру не переводил, и не буду переводить, не переношу пиксельную графику. О чём я в начале и написал. И играть в неё не буду, ни с матом, ни без. Поэтому мне всё равно какой вы там перевод сделаете. Игры с матом мне тоже не нравятся, о чём я тоже выше написал. Заканчивайте тут флудить и оффтопить. Именно за это вам и минусы от меня под вашими постами. А не в ответ “из принципа”. Не только я вам написал, что нужно вам поубавить гонору, наверно надо как-то адекватнее прислушаться к тому, что вам уже несколько человек написали… Переводы вашей команды, в целом, очень хорошие и качественные. Успехов вам в вашей работе и в переводе этой игры.
    • короче немогу вытащить из свитч версии локализационный файл, немогу понять как патчится клиент у свитча на эмуляторе
    • Пушкин победил?)))) Много слов, а сами себе противоречите, не можете сделать так, как вам же нравится, а другим какую-то чушь пишете. Жалуетесь на минусы? А сами перед этим то же самое сделали))) Как вы к другим, так и к вам, привыкайте. Оскорблять кто начал первым и на личности переходить? В отличие от вас, я лично никого не оскорблял, даже после ваших тупых сообщений и наездов. Смешно же от вас, не более.
    • А неплохо стартовали, уже за 8 тыс.
    • @allodernat ну, тогда значит перевод пока без мата. Это и хорошо.  Не знаю чем он там переводил, возможно Яндексом.
  • Изменения статусов

    • Дмитрий Соснов  »  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 | Реклама на сайте.

×