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

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

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

Ultimate Steam Enhancer || Tamper Monkey

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

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

Функционал скрипта:

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

1. Индикаторы перевода на русский язык и дополнительные обзоры на странице игры:

  • Отображаются значки с информацией о наличии русского интерфейса, озвучки и субтитров
  • Добавляются дополнительные данные об обзорах (тотальные, тотальные без китайских, только русские)
  • Возможность просмотра актуальных русскоязычных обзоров в модальном окне при щелчке по “русские”
Скрытый текст

LuURig3.png

FAPeJiW.png

 

 

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

2. Информация о времени прохождения игры с HLTB (How Long To Beat) на странице игры:

  • На странице игры отображается блок с информацией о времени прохождения, полученной с сайта How Long To Beat. Включает данные о времени прохождения для:
    • Только сюжета
    • Сюжета + дополнений
    • 100% прохождения
    • Всех стилей игры
  • Время прохождения отображается в часах (если время прохождения менее часа, то в минутах), а также указывается количество человек, на основе прохождения которых рассчитаны данные.
  • Поскольку поиск времени происходит по имени, можно выбрать нужную игру из вариантов.
Скрытый текст

7KhznYc.png

6tgxA2s.png

 

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

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

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

V6Op5Eb.png

XgAVWAp.png

qJK0IjU.png

 

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

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

  • При наведении мыши слева появляется всплывающая подсказка с детальной информацией о игре
  • Включает: издателей, разработчиков, серию игр, отзывы, информацию о раннем доступе, поддержку русского [и английского] языка, метки и краткое описание
  • В каталоге поиска справа находится меню “Русский перевод”:
    • Только текст (оставляет игры с русским интерфейсом/субтитрами без озвучки)
    • Озвучка (оставляет игры, где обязательно есть русская озвучка)
    • Без перевода (оставляет игры, где нет русского)
  • В каталоге поиска справа находится меню DLC:
    • Только ваши DLC: показывает только дополнения для игр из вашей библиотеки (подсвечиваются фиолетовым).
Скрытый текст

Пример дополнительной информации

nLfsBzR.png

 

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

5. Система скрытия игр при поиске по каталогу:

  • В левом углу на странице игры появляется информация о текущем количестве показанных игр
  • У каждой игры появляется чекбокс для отметки игр для последующего скрытия
  • Кнопка "Скрыть выбранное" позволяет удалить отмеченные игры из результатов поиска и внести их в официальный список скрытых игр Steam вашего аккаунта (вернуть их можно через настройки аккаунта Steam).
  • В отличие от встроенной системы скрытия игр, мой скрипт не скрывает игры косметически, просто делая их невидимыми для пользователя и засоряя код страницы — он удаляет элемент целиком, тем самым повышая производительность при долгом скроллинге.
Скрытый текст

uCA8x2P.png

 

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

6. Система скрытия новостей в новостном центре:

  • Для каждой новости добавляется чекбокс для последующего скрытия.
  • Кнопка "Скрыть" позволяет удалить отмеченные новости.
  • Скрытые новости сохраняются в локальном хранилище и не показываются при повторном посещении страницы (очистка файлов cookie приведёт к очистке локального хранилища и потере информации о скрытых новостях).
Скрытый текст

5lPQZUO.png

 

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

7. Информация об исторических продаж предмета на торговой площадке Steam:

  • На странице предмета на торговой площадке Steam добавляется блок с информацией о продажах.
  • Информация о продажах представлена в удобном формате таблицы, где каждая строка соответствует отдельному году. Для каждого года отображается:
    • Сумма продаж за год (в рублях).
    • Сумма, полученная разработчиком (в рублях, рассчитанная как 66.67% от комиссии Steam).
    • Сумма, полученная Valve (в рублях, рассчитанная как 33.33% от комиссии Steam).
Скрытый текст

QW7qsp1.png

ZPnzyNH.png

 

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

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

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

FUKK7WX.png

73ZFMvx.png

jCbsbEV.png

 

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

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 уведомлений. Но сомневаюсь, что кому-то понадобится больше.
Скрытый текст

ajDr2rp.png

jNSTM9c.png

Bv5Be9Y.png

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

10. На странице игры отображается блок с информацией о времени, которое в ней провели друзья, а также о статистике глобальных достижений.

  • Время друзей включает:
    • Максимальное время прохождения (И ник друга со ссылкой).
    • Среднее время прохождения (и указание количества друзей, по которым высчитывалось среднее).
    • Минимальное время прохождения.
  • Глобальные достижения включают:
    • Процент платины.
    • Средний прогресс.

9TaMCbZ.png

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

11. На странице игры отображается кнопка "Цены (VGT)" (находится в блоке с "в желаемое", "подписаться" и "скрыть") для отображения цен из магазинов.

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

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

Важно!

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

7cJzB8a.png

fNRnaHw.png

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

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

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

SPzJrpi.png

6iGlcTf.png

 

Скрытый текст
  1. На странице игры Steam рядом с кнопкой "Цены (VGT)" появляется кнопка "Plati":
    lyL8i5g.png
     
  • При нажатии открывается полноэкранное модальное окно для поиска предложений на торговой площадке Plati.Market.
  • Функционал включает:
    • Автоматическое заполнение строки поиска названием текущей игры.
    • Возможность редактировать поисковый запрос и запускать поиск вручную.
    • Подсказки при вводе запроса (на основе API Plati).
    • Сортировку результатов по:
      • Цене (возрастание/убывание).
      • Количеству продаж (убывание/возрастание).
      • Релевантности (порядок от API Plati).
      • Дополнительным параметрам (название, дата добавления, рейтинг продавца, отзывы и др.).
    • Фильтрацию результатов по:
      • Диапазону цен (в выбранной валюте: RUR, USD, EUR, UAH).
      • Диапазону количества продаж.
      • Диапазону рейтинга продавца.
      • Опциям (скрыть с плохими отзывами, скрыть с возвратами, участие продавца в системе скидок).
      • Дате добавления товара.
    • Исключение товаров из результатов по ключевым словам (добавляются в панель справа).
    • Сохранение состояния фильтров, сортировки, выбранной валюты и списка исключений между сессиями.

 

Инструкция по использованию:

Скрытый текст
  1. Установите расширение для пользовательских скриптов (например, Tampermonkey или Greasemonkey)
  2. Создайте новый пользовательский скрипт и вставьте код
  3. Настройте параметры работы скрипта через переменную scriptsConfig:
    
    // Основные скрипты
    gamePage: true, // Скрипт для страницы игры (индикаторы о наличии русского перевода; получение дополнительных обзоров) | https://store.steampowered.com/app/*
    hltbData: true, // Скрипт для страницы игры (HLTB; получение сведений о времени прохождения) | https://store.steampowered.com/app/*
    friendsPlaytime: true, // Скрипт для страницы игры (Время друзей & Достижения) | https://store.steampowered.com/app/*
    earlyaccdata: true,
    zogInfo: true, // Скрипт для страницы игры (ZOG; получение сведение о наличии русификаторов) | https://store.steampowered.com/app/*
    vgtSales: true, // Скрипт для страницы игры (VGT; отображения цен из агрегатора VGTimes) | https://store.steampowered.com/app/*
    catalogInfo: true, // Скрипт для получения дополнительной информации об игре при наведении на неё на странице поиска по каталогу | https://store.steampowered.com/search/
    catalogHider: false, // Скрипт скрытия игр на странице поиска по каталогу | https://store.steampowered.com/search/
    newsFilter: true, // Скрипт для скрытия новостей в новостном центре: | https://store.steampowered.com/news/
    Kaznachei: true, // Скрипт для показа годовых и исторических продаж предмета на торговой площадке Steam | https://steamcommunity.com/market/listings/*
    homeInfo: true, // Скрипт для получения дополнительной информации об игре при наведении на неё на странице вашей активности Steam | https://steamcommunity.com/my/
    wishlistTracker: true, // Скрипт для получения уведомлений об изменении дат выхода игр из вашего списка желаемого Steam и показа календаря с датами | https://steamcommunity.com/my/wishlist/
    // Дополнительные настройки
    autoExpandHltb: false, // Автоматически раскрывать спойлер HLTB
    autoLoadReviews: false, // Автоматически загружать дополнительные обзоры
    toggleEnglishLangInfo: false // Отображает данные об английском языке в дополнительной информации при поиске по каталогу и в активности (функция для переводчиков)

     

  4. Использование функций:

    • На странице игры, справа от изображения, автоматически появятся индикаторы перевода. Кнопка загрузки дополнительных обзоров появится под коротким описанием.
    • На странице игры, справа от короткого описания и под индикаторами перевода появится спойлер с информацией о времени прохождения (HLTB).
    • На странице игры, справа от короткого описания и правее индикаторов перевода появится спойлер с информацией о времени прохождения друзей и достижениями.
    • На странице игры, справа от короткого описания и выше индикаторов перевода появится информация о времени нахождения игры в раннем доступе.
    • На странице игры, справа от короткого описания и под блоком HLTB появится спойлер с информацией о наличии переводов с ZOG. Если игра не была найдена по App ID, то скрипт попытается найти её по имени, после чего вы сможете щёлкнуть по наиболее подходящему вам варианту.
    • На странице игры, в блоке с кнопками "В желаемое", "Подписаться" и "Скрыть" появятся кнопки "Цены (VGT)" и "Plati":
      • При нажатии "Цены (VGT)" откроется модальное окно с ценами из агрегатора VGTimes.
      • При нажатии "Plati" откроется полноэкранное модальное окно с поиском по Plati.Market, фильтрами и сортировкой.
    • В каталоге поиска при наведении на игру будет показываться дополнительная информация.
    • В каталоге поиска справа находится меню “Русский перевод”:
      • Только текст (оставляет игры с русским интерфейсом/субтитрами без озвучки)
      • Озвучка (оставляет игры, где обязательно есть русская озвучка)
      • Без перевода (оставляет игры, где нет русского)
    • В каталоге поиска справа находится меню “DLC”:
      • Только ваши DLC: показывает только дополнения для игр из вашей библиотеки (подсвечиваются фиолетовым).
    • Для скрытия игр используйте чекбоксы и кнопку "Скрыть выбранное"
    • Для фильтрации новостей используйте чекбоксы и кнопку "Скрыть".
    • Для просмотра истории продаж предмета зайдите на его страницу и раскройте спойлер “Информация о продажах”.
    • В вашей активности при наведении на игру будет показываться дополнительная информация.
    • На любой странице в правом углу будет кнопка “Отслеживание вишлиста.
      • При первом запуске щёлкните по ней, нажмите обновить и дождитесь завершения.
      • Затем можете следит за индикатором устаревания информации.
      • Когда вам кажется, что можно запросить новые данные — снова нажмите обновить.
      • Жмите кнопку конверта, чтобы прочитать уведомление или крестик для удаления.
      • Используйте кнопку очистить для удаления всех уведомлений.


P.S. Рекомендую отключать catalogHider, если у вас нет задачи массового перебора игр.


Поскольку вес кода превысил 140 кб (в 3000+ строках), залил его на внешний ресурс:

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

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

 

 

 

 

 

 

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

Версия 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 для расчета.
    • Добавлены кнопки для фильтрации игр по этому критерию: "< РРЦ", "= РРЦ", "> РРЦ".
    • Примечание: Эта функция требует получения начальных цен как для российского, так и для американского региона Steam через API, что может увеличить время обработки. РРЦ рассчитывается на основе текущей полной (не скидочной) цены игры в долларах США и таблицы рекомендованных цен Steam.
  • (Новое!) Фильтры по типу скидок и проценту от исторического минимума (ATL):
    Стандартный блок фильтров скидок SteamDB заменен на более продвинутый:
    • Фильтры по типу исторической цены: Позволяют отображать или скрывать игры на основе цвета их скидки на SteamDB (синий - новый ист. минимум, зеленый - повтор мин. цены, фиолетовый - мин. за 2 года).
    • Фильтры по процентам в историческом минимуме (ATL): Сравнивают текущий процент скидки с процентом скидки, который был при достижении игрой All-Time Low цены. Позволяют отфильтровать игры, которые сейчас выгоднее/так же/менее выгодны по проценту скидки, чем были при своем ATL.
      • % < Минимума: Текущий % скидки > Исторического % ATL (выгоднее).
      • % = Минимуму: Текущий % скидки = Историческому % ATL.
      • % > Минимума: Текущий % скидки < Исторического % ATL (менее выгодно по % скидки).
Скрытый текст

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 и лучшей ценой для расчета.
  • Позволяет сортировать таблицу по любому столбцу.
  • Рассчитывает и показывает итоговые суммы для покупки всех игр по ~полным ценам и по лучшим доступным ценам.

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

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

Инструкция по использованию:

Скрытый текст
  1. Зайдите на страницу Sales на SteamDB (или любую другую, где работает скрипт, например, Most Followed или ваш Wishlist при активном соответствующем фильтре на SteamDB).
  2. Проставьте нужную валюту и основные фильтры SteamDB, если необходимо.
    • Рекомендуется установить количество обзоров >100 и рейтинг больше 70 или 80.
    • Учтите, что чем больше игр отображается, тем больше запросов будет отправлено к Steam для получения данных (в одном запросе содержится до 200 игр).
    • По внутренним ограничениям SteamDB система не может вывести больше 10 тысяч игр.
  3. Нажмите кнопку "Обработать игры" (или "Высчитать" для калькулятора желаемого) в панели скрипта.
    • Скрипт автоматически выберет "All (slow)" в выпадающем списке "entries per page" и покажет таймер обратного отсчета до начала сбора данных. Это необходимо для загрузки всех игр на страницу.
    • Дождитесь завершения обработки. Статус будет отображаться на кнопке и в индикаторе.
    • Если выбрана российская валюта (для анализа РРЦ), сбор данных будет проходить в два этапа (RU и US цены), что может занять больше времени.
    • Для других валют (или если анализ РРЦ не требуется/невозможен) сбор данных пройдет в один этап.
Скрытый текст
  • Русский перевод:
    • Если вы хотите оставить игры только с текстовым переводом, выберите "Только текст".
    • Если хотите оставить игры с озвучкой, выберите "Озвучка".
    • Если хотите оставить игры без русского перевода, используйте кнопку "Без перевода".
  • Фильтр по дате:
    • Если вы хотите оставить игры, скидка на которые появилась после определённой даты, нажмите по значку календаря, выберите нужную дату и нажмите "Фильтр по дате".
  • Фильтр списков (для недоступных игр):
    1. Выберите валюту первого региона и примените, чтобы страница обновилась.
    2. Нажмите на кнопку "Обработать игры". Скрипт автоматически выберет "All entries" и после загрузки и обработки данных, нажмите кнопку "Список 1" — вы получите уведомление, что список сохранён.
    3. Выберите валюту второго региона и примените, чтобы страница обновилась.
    4. Нажмите на кнопку "Обработать игры". После загрузки и обработки, нажмите кнопку "Список 2".
    5. Нажмите на кнопку "Фильтр списков", чтобы скрыть игры, присутствующие в обоих списках, и оставить только уникальные для текущего региона игры.
  • Фильтр РРЦ (РФ):
    • Этот фильтр активен только при выборе российской валюты.
    • Нажмите на кнопки "< РРЦ", "= РРЦ" или "> РРЦ" для отображения игр, чья цена ниже, соответствует или выше рекомендованной Steam цены соответственно. Можно выбрать несколько критериев одновременно.
    • Повторное нажатие на кнопку снимает соответствующий фильтр.
  • (Новое!) Фильтры по скидкам SteamDB (заменяют стандартные):
    • Используйте чекбоксы в разделе "Фильтры по скидкам" на панели SteamDB (справа) для выбора нужных критериев.
    • Фильтры по типу исторической цены: Отмечайте чекбоксы "Показать" или "Скрыть" для категорий "Ист. минимум", "Повтор мин. цены", "Мин. за 2 года".
    • Фильтры по процентам в ATL: Аналогично, используйте чекбоксы "Показать" или "Скрыть" для категорий "% < Минимума", "% = Минимуму", "% > Минимума".
    • Фильтры применяются немедленно.
Скрытый текст
  • Введите нужный вам курс в соответствующее поле.
  • Нажмите "Конвертировать".
  • Цены в таблице будут пересчитаны. Рекомендуется производить конвертацию до применения других фильтров, так как скрипт конвертирует цены только у тех игр, которые отображаются в данный момент.
Скрытый текст
  • Для получения дополнительной информации об игре наведите на неё курсор — подсказка выведется справа.
  • Индикатор РРЦ (РФ): После успешной обработки игр (при активной российской валюте) слева от названия каждой игры появится индикатор сравнения текущей цены с рекомендованной Steam.
  • (Новое!) Визуализация процента скидки ATL: В информации об All-Time Low (под названием игры), текст "at -X%" будет подсвечен цветом, указывающим на выгодность текущего процента скидки по сравнению с историческим процентом ATL.
  • Если вы хотите включить отображение информации об английском языке в игре, в настройке скрипта (в коде) проставьте true в:

scriptsConfig.toggleEnglishLangInfo: false

 

Скриншоты:

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

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

BNZCExN.png

 


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

hcu3kck.png

 


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

s53CiaE.png

 

Для облегчения страницы форума, залил скрипт на внешний ресурс:

Код скрипта на 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

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


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

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

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

 

Описание:

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

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

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

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

EfYEeQs.png

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


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

(function() {
    'use strict';

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

    const scriptsConfig = {
        toggleEnglishLangInfo: false
    };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        applyCurrentFilter();
    }

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

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

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

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

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

            processedGames++;
            updateProgress();
        });

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

        if (activeFilter) applyCurrentFilter();
    }

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

        isProcessingQueue = true;

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

        isProcessingQueue = false;
    }

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

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

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

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

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

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

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

        const rect = element.getBoundingClientRect();
        tooltip.style.left = `${rect.right + window.scrollX}px`;
        tooltip.style.top = `${rect.top + window.scrollY - 8}px`;
        tooltip.style.opacity = '1';
    }

    function buildTooltipContent(data) {
        const reviewClass = getReviewClass(data.percent_positive, data.review_count);
        const earlyAccessClass = data.is_early_access ? 'early-access-yes' : 'early-access-no';

        let languageSupportRussianText = "Отсутствует";
        let languageSupportRussianClass = 'language-no';
        if (data.language_support_russian) {
            languageSupportRussianText = "";
            if (data.language_support_russian.supported) languageSupportRussianText += "<br>Интерфейс: ✔ ";
            if (data.language_support_russian.full_audio) languageSupportRussianText += "<br>Озвучка: ✔ ";
            if (data.language_support_russian.subtitles) languageSupportRussianText += "<br>Субтитры: ✔";
            languageSupportRussianClass = languageSupportRussianText ? 'language-yes' : 'language-no';
        }

        let languageSupportEnglishText = "Отсутствует";
        let languageSupportEnglishClass = 'language-no';
        if (data.language_support_english) {
            languageSupportEnglishText = "";
            if (data.language_support_english.supported) languageSupportEnglishText += "<br>Интерфейс: ✔ ";
            if (data.language_support_english.full_audio) languageSupportEnglishText += "<br>Озвучка: ✔ ";
            if (data.language_support_english.subtitles) languageSupportEnglishText += "<br>Субтитры: ✔";
            languageSupportEnglishClass = languageSupportEnglishText ? 'language-yes' : 'language-no';
        }

        return `
            <div class="group-top">
                <div class="tooltip-row compact"><strong>Серия игр:</strong> <span class="${!data.franchises ? 'no-data' : ''}">${data.franchises || "Нет данных"}</span></div>
            </div>
            <div class="group-middle">
                <div class="tooltip-row spaced"><strong>Отзывы:</strong> <span class="${reviewClass}">${data.percent_positive || "0"}%</span> (${data.review_count || "0"})</div>
                <div class="tooltip-row spaced"><strong>Ранний доступ:</strong> <span class="${earlyAccessClass}">${data.is_early_access ? "Да" : "Нет"}</span></div>
            </div>
            <div class="group-bottom">
                <div class="tooltip-row language"><strong>Русский язык:</strong> <span class="${languageSupportRussianClass}">${languageSupportRussianText}</span></div>
                ${scriptsConfig.toggleEnglishLangInfo ? `
                    <div class="tooltip-row language"><strong>Английский язык:</strong> <span class="${languageSupportEnglishClass}">${languageSupportEnglishText}</span></div>
                ` : ''}
            </div>
            <div class="tooltip-row description"><strong>Описание:</strong> <span class="${!data.short_description ? 'no-data' : ''}">${data.short_description || "Нет данных"}</span></div>
        `;
    }

    function getReviewClass(percent, totalReviews) {
        if (totalReviews === 0) return 'no-reviews';
        if (percent >= 70) return 'positive';
        if (percent >= 40) return 'mixed';
        return 'negative';
    }

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

        clearTimeout(hoverTimer);

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

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

    function init() {
        const style = document.createElement('style');
        style.textContent = `
            .steamdb-tooltip {
                position: absolute;
                background: #1b2838;
                color: #c6d4df;
                padding: 15px;
                border-radius: 3px;
                width: 320px;
                font-size: 14px;
                line-height: 1.5;
                box-shadow: 0 0 12px rgba(0,0,0,0.5);
                opacity: 0;
                transition: opacity 0.2s;
                pointer-events: none;
                z-index: 9999;
            }
            .tooltip-arrow {
                position: absolute;
                left: -10px;
                top: 20px;
                width: 0;
                height: 0;
                border-top: 10px solid transparent;
                border-bottom: 10px solid transparent;
                border-right: 10px solid #1b2838;
            }
            .group-top {
                margin-bottom: 8px;
            }
            .group-middle {
                margin-bottom: 12px;
            }
            .group-bottom {
                margin-bottom: 15px;
            }
            .tooltip-row.compact {
                margin-bottom: 2px;
            }
            .tooltip-row.spaced {
                margin-bottom: 10px;
            }
            .tooltip-row.language {
                margin-bottom: 8px;
            }
            .tooltip-row.description {
                margin-top: 15px;
                padding-top: 10px;
                border-top: 1px solid #2a3a4d;
                color: #8f98a0;
                font-style: italic;
            }
            .positive { color: #66c0f4; }
            .mixed { color: #997a00; }
            .negative { color: #a74343; }
            .no-reviews { color: #929396; }
            .language-yes { color: #66c0f4; }
            .language-no { color: #a74343; }
            .early-access-yes { color: #66c0f4; }
            .early-access-no { color: #929396; }
            .no-data { color: #929396; }
        `;

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

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

        collectAppIds();
    }

    init();
})();

 


 

 

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

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


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

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

Цитата

Технобог

Факт!

  • Лайк (+1) 1

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


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

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

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

2025-03-06-185528.jpg

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

 

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


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

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

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

2025-03-06-185528.jpg

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

 

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

aoyCRUQ.png

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

tbOl5AG.png

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

UZKyDBz.png


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

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

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

(function() {
    'use strict';

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

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

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

        return container;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        button:hover {
            background: #4f9fd1;
        }


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

 

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

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

67wLCzy.png

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

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


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

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

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

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

(function() {
    'use strict';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();

 

 

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

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


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

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

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


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

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

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

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

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


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

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

  • Лайк (+1) 1

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


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

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

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

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

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


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

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

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


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

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

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

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

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

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

Войти

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

Войти сейчас



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

×