您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Добавляет множество функций для улучшения взаимодействия с магазином и сообществом (Полный список на странице скрипта)
当前为
- // ==UserScript==
- // @name Ultimate Steam Enhancer
- // @namespace https://store.steampowered.com/
- // @version 1.8
- // @description Добавляет множество функций для улучшения взаимодействия с магазином и сообществом (Полный список на странице скрипта)
- // @author 0wn3df1x
- // @license MIT
- // @require https://code.jquery.com/jquery-3.6.0.min.js
- // @match https://store.steampowered.com/*
- // @match *://*steamcommunity.com/*
- // @grant GM_xmlhttpRequest
- // @grant GM_getResourceText
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addStyle
- // @connect zoneofgames.ru
- // @connect raw.githubusercontent.com
- // @connect gist.githubusercontent.com
- // @connect store.steampowered.com
- // @connect api.steampowered.com
- // @connect steamcommunity.com
- // @connect shared.cloudflare.steamstatic.com
- // @connect umadb.ro
- // @connect api.github.com
- // @connect howlongtobeat.com
- // ==/UserScript==
- (function() {
- 'use strict';
- const scriptsConfig = {
- // Основные скрипты
- gamePage: true, // Скрипт для страницы игры (индикаторы о наличии русского перевода; получение дополнительных обзоров) | https://store.steampowered.com/app/*
- hltbData: true, // Скрипт для страницы игры (HLTB; получение сведений о времени прохождения) | https://store.steampowered.com/app/*
- friendsPlaytime: true, // Скрипт для страницы игры (Время друзей & Достижения) | https://store.steampowered.com/app/*
- zogInfo: true, // Скрипт для страницы игры (ZOG; получение сведение о наличии русификаторов) | 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 // Отображает данные об английском языке в дополнительной информации при поиске по каталогу и в активности (функция для переводчиков)
- };
- // Скрипт для страницы игры (индикаторы о наличии русского перевода; получение дополнительных обзоров) | https://store.steampowered.com/app/*
- if (scriptsConfig.gamePage && window.location.pathname.includes('/app/')) {
- (function() {
- 'use strict';
- function createFruitIndicator(apple, hasSupport, orange) {
- const banana = document.createElement('div');
- banana.style.position = 'relative';
- banana.style.cursor = 'pointer';
- const grape = document.createElement('div');
- grape.style.width = '60px';
- grape.style.height = '60px';
- grape.style.borderRadius = '4px';
- grape.style.display = 'flex';
- grape.style.alignItems = 'center';
- grape.style.justifyContent = 'center';
- grape.style.background = hasSupport ? 'rgba(66, 135, 245, 0.2)' : 'rgba(0, 0, 0, 0.1)';
- grape.style.border = `1px solid ${hasSupport ? '#2A5891' : '#3c3c3c'}`;
- grape.style.opacity = '0.95';
- grape.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
- grape.style.overflow = 'hidden';
- grape.style.position = 'relative';
- grape.style.transform = 'translateZ(0)';
- const kiwi = document.createElement('div');
- kiwi.innerHTML = apple;
- kiwi.style.width = '30px';
- kiwi.style.height = '30px';
- kiwi.style.display = 'block';
- kiwi.style.margin = '0 auto';
- kiwi.style.transition = 'fill 0.3s ease';
- grape.appendChild(kiwi);
- const svgElement = kiwi.querySelector('svg');
- function setColor(hasSupport) {
- const borderColor = hasSupport ? '#2A5891' : '#3c3c3c';
- const svgFill = hasSupport ? '#FFFFFF' : '#0E1C25';
- grape.style.border = `1px solid ${borderColor}`;
- svgElement.style.fill = svgFill;
- }
- setColor(hasSupport);
- const pineapple = document.createElement('div');
- const hasLabel = hasSupport ? orange : getGenitiveCase(orange);
- pineapple.textContent = hasSupport ? `Есть ${orange}` : `Нет ${hasLabel}`;
- pineapple.style.position = 'absolute';
- pineapple.style.top = '50%';
- pineapple.style.left = '100%';
- pineapple.style.transform = 'translateY(-50%) translateX(10px)';
- pineapple.style.background = 'rgba(0, 0, 0, 0.8)';
- pineapple.style.color = '#fff';
- pineapple.style.padding = '8px 12px';
- pineapple.style.borderRadius = '8px';
- pineapple.style.fontSize = '14px';
- pineapple.style.whiteSpace = 'nowrap';
- pineapple.style.opacity = '0';
- pineapple.style.transition = 'opacity 0.3s ease';
- pineapple.style.zIndex = '10000';
- pineapple.style.pointerEvents = 'none';
- banana.appendChild(pineapple);
- banana.addEventListener('mouseenter', () => {
- grape.style.transform = 'scale(1.1) translateZ(0)';
- pineapple.style.opacity = '1';
- });
- banana.addEventListener('mouseleave', () => {
- grape.style.transform = 'scale(1) translateZ(0)';
- pineapple.style.opacity = '0';
- });
- banana.appendChild(grape);
- return banana;
- }
- function getGenitiveCase(orange) {
- switch (orange) {
- case 'интерфейс':
- return 'интерфейса';
- case 'озвучка':
- return 'озвучки';
- case 'субтитры':
- return 'субтитров';
- default:
- return orange;
- }
- }
- function checkRussianSupport() {
- const mango = document.querySelector('#languageTable table.game_language_options');
- if (!mango) return {
- interface: false,
- voice: false,
- subtitles: false
- };
- const strawberry = mango.querySelectorAll('tr');
- for (let blueberry of strawberry) {
- const watermelon = blueberry.querySelector('td.ellipsis');
- if (watermelon && /русский|Russian/i.test(watermelon.textContent.trim())) {
- const cherry = blueberry.querySelector('td.checkcol:nth-child(2) span');
- const raspberry = blueberry.querySelector('td.checkcol:nth-child(3) span');
- const blackberry = blueberry.querySelector('td.checkcol:nth-child(4) span');
- return {
- interface: cherry !== null,
- voice: raspberry !== null,
- subtitles: blackberry !== null
- };
- }
- }
- return {
- interface: false,
- voice: false,
- subtitles: false
- };
- }
- function addRussianIndicators() {
- const russianSupport = checkRussianSupport();
- if (!russianSupport) return;
- let lemon = document.querySelector('#gameHeaderImageCtn');
- if (!lemon) return;
- const lime = document.createElement('div');
- lime.style.position = 'absolute';
- lime.style.top = '-10px';
- lime.style.left = 'calc(100% + 10px)';
- lime.style.display = 'flex';
- lime.style.flexDirection = 'column';
- lime.style.gap = '15px';
- lime.style.alignItems = 'flex-start';
- lime.style.zIndex = '2';
- lime.style.marginTop = '10px';
- const peach = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12,0C5.38,0,0,5.38,0,12s5.38,12,12,12s12-5.38,12-12S18.62,0,12,0z M12,22C6.49,22,2,17.51,2,12S6.49,2,12,2 s10,4.49,10,10S17.51,22,12,22z M10.5,10h3v8h-3V10z M10.5,5h3v3h-3V5z" /></svg>`, russianSupport.interface, 'интерфейс');
- const plum = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15,21v-2c3.86,0,7-3.14,7-7s-3.14-7-7-7V3c4.96,0,9,4.04,9,9S19.96,21,15,21z M15,17v-2c1.65,0,3-1.35,3-3s-1.35-3-3-3V7 c2.76,0,5,2.24,5,5S17.76,17,15,17z M1,12v4h5l6,5V3L6,8H1V12" /></svg>`, russianSupport.voice, 'озвучка');
- const apricot = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M11,24l-4.4-5H0V0h23v19h-7.6L11,24z M2,17h5.4l3.6,4l3.6-4H21V2H2V17z" /></g><g><rect x="5" y="8" width="3" height="3" /></g><g><rect x="10" y="8" width="3" height="3" /></g><g><rect x="15" y="8" width="3" height="3" /></g></svg>`, russianSupport.subtitles, 'субтитры');
- lime.appendChild(peach);
- lime.appendChild(plum);
- lime.appendChild(apricot);
- lemon.style.position = 'relative';
- lemon.appendChild(lime);
- const appName = document.querySelector('#appHubAppName.apphub_AppName');
- if (appName) {
- appName.style.maxWidth = '530px';
- appName.style.overflow = 'hidden';
- appName.style.textOverflow = 'ellipsis';
- appName.style.whiteSpace = 'nowrap';
- appName.title = appName.textContent;
- }
- }
- const settings = {
- showTotalReviews: true,
- showNonChineseReviews: true,
- showRussianReviews: true
- };
- function fetchReviews(appid, language, callback) {
- let url = `https://store.steampowered.com/appreviews/${appid}?json=1&language=${language}&purchase_type=all`;
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- onload: function(response) {
- let data = JSON.parse(response.responseText);
- callback(data);
- }
- });
- }
- function fetchRussianReviewsHTML(appid, filter, callback) {
- let url = `https://store.steampowered.com/appreviews/${appid}?language=russian&purchase_type=all&filter=${filter}&day_range=365`;
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- onload: function(response) {
- let data = JSON.parse(response.responseText);
- callback(data.html);
- }
- });
- }
- function addStyles() {
- GM_addStyle(`
- .additional-reviews {
- margin-top: 10px;
- }
- .additional-reviews .user_reviews_summary_row {
- display: flex;
- line-height: 16px;
- cursor: pointer;
- margin-bottom: 5px;
- }
- .additional-reviews .subtitle {
- flex: 1;
- color: #556772;
- font-size: 12px;
- }
- .additional-reviews .summary {
- flex: 3;
- color: #c6d4df;
- font-size: 12px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .additional-reviews .game_review_summary {
- font-weight: normal;
- }
- .additional-reviews .positive {
- color: #66c0f4;
- }
- .additional-reviews .mixed {
- color: #B9A074;
- }
- .additional-reviews .negative {
- color: #a34c25;
- }
- .additional-reviews .no_reviews {
- color: #929396;
- }
- .additional-reviews .responsive_hidden {
- color: #556772;
- margin-left: 5px;
- }
- .ofxmodal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgba(0,0,0,0.8);
- }
- .ofxmodal-content {
- background-color: #1b2838;
- margin: 10% auto;
- padding: 20px;
- border: 1px solid #888;
- width: 80%;
- max-width: 800px;
- color: #c6d4df;
- position: relative;
- max-height: 80vh;
- overflow-y: auto;
- }
- .ofxclose {
- color: #aaa;
- position: sticky;
- top: 0;
- float: right;
- font-size: 28px;
- font-weight: bold;
- cursor: pointer;
- background: rgba(0,0,0,0.8);
- padding: 5px 10px;
- border-radius: 5px;
- transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
- }
- .ofxclose:hover {
- color: #fff;
- background: #e64a4a;
- transform: scale(1.1);
- }
- .ofxclose:active {
- background: #c43c3c;
- transform: scale(0.95);
- }
- .refresh-button {
- position: left;
- top: 10px;
- right: 50px;
- background: #66c0f4;
- color: #1b2838;
- padding: 10px 20px;
- border: none;
- cursor: pointer;
- z-index: 1001;
- border-radius: 2px;
- transition: background 0.2s ease, color 0.2s ease;
- }
- .refresh-button:hover {
- background: #45b0e6;
- color: #fff;
- }
- .refresh-button:active {
- background: #329cd4;
- transform: translateY(1px);
- }
- `);
- }
- function formatNumber(number) {
- return number.toLocaleString('en-US');
- }
- function getReviewClass(percent, totalReviews) {
- if (totalReviews === 0) return 'no_reviews';
- if (percent >= 70) return 'positive';
- if (percent >= 40) return 'mixed';
- if (percent >= 1) return 'negative';
- return 'negative';
- }
- function addLoadButton() {
- let reviewsContainer = document.querySelector('.user_reviews');
- if (reviewsContainer) {
- let additionalReviews = document.createElement('div');
- additionalReviews.className = 'additional-reviews';
- additionalReviews.innerHTML = `
- <div class="user_reviews_summary_row" id="load-reviews-button">
- <div class="subtitle column all">Доп. обзоры:</div>
- <div class="summary column">
- <span class="game_review_summary no_reviews">Загрузить</span>
- </div>
- </div>
- `;
- reviewsContainer.appendChild(additionalReviews);
- document.getElementById('load-reviews-button').addEventListener('click', function() {
- loadAdditionalReviews();
- });
- if (scriptsConfig.autoLoadReviews) {
- loadAdditionalReviews();
- }
- }
- }
- function loadAdditionalReviews() {
- let appid = window.location.pathname.match(/\/app\/(\d+)/)[1];
- let languages = [];
- let data = {};
- let loadButton = document.getElementById('load-reviews-button');
- if (loadButton) {
- loadButton.querySelector('.game_review_summary').textContent = 'Загрузка...';
- }
- if (settings.showTotalReviews || settings.showNonChineseReviews) {
- languages.push('all');
- }
- if (settings.showNonChineseReviews) {
- languages.push('schinese');
- }
- if (settings.showRussianReviews) {
- languages.push('russian');
- }
- languages.forEach(language => {
- fetchReviews(appid, language, (response) => {
- data[language] = response;
- if (Object.keys(data).length === languages.length) {
- displayAdditionalReviews(data['all'], data['schinese'], data['russian']);
- if (loadButton) {
- loadButton.querySelector('.game_review_summary').textContent = 'Загрузить';
- }
- }
- });
- });
- }
- function displayAdditionalReviews(allData, schineseData, russianData) {
- let allReviews = allData ? allData.query_summary : null;
- let schineseReviews = schineseData ? schineseData.query_summary : null;
- let russianReviews = russianData ? russianData.query_summary : null;
- let additionalReviews = document.querySelector('.additional-reviews');
- if (additionalReviews) {
- additionalReviews.innerHTML = '';
- if (settings.showTotalReviews && allReviews) {
- let allPercent = allReviews.total_reviews > 0 ? Math.round((allReviews.total_positive / allReviews.total_reviews) * 100) : 0;
- let allClass = getReviewClass(allPercent, allReviews.total_reviews);
- additionalReviews.innerHTML += `
- <div class="user_reviews_summary_row">
- <div class="subtitle column all">Тотальные:</div>
- <div class="summary column">
- <span class="game_review_summary ${allClass}">${allPercent}% из ${formatNumber(allReviews.total_reviews)} положительные</span>
- </div>
- </div>
- `;
- }
- if (settings.showNonChineseReviews && allReviews && schineseReviews) {
- let schintotalrev = allReviews.total_reviews - schineseReviews.total_reviews;
- let schintotapos = allReviews.total_positive - schineseReviews.total_positive;
- let schinpercent = schintotalrev > 0 ? Math.round((schintotapos / schintotalrev) * 100) : 0;
- let schinClass = getReviewClass(schinpercent, schintotalrev);
- additionalReviews.innerHTML += `
- <div class="user_reviews_summary_row">
- <div class="subtitle column all">Безкитайские:</div>
- <div class="summary column">
- <span class="game_review_summary ${schinClass}">${schinpercent}% из ${formatNumber(schintotalrev)} положительные</span>
- </div>
- </div>
- `;
- }
- if (settings.showRussianReviews && russianReviews) {
- let rustotalrev = russianReviews.total_reviews;
- let ruspositive = russianReviews.total_positive;
- let ruspercent = rustotalrev > 0 ? Math.round((ruspositive / rustotalrev) * 100) : 0;
- let rusClass = getReviewClass(ruspercent, rustotalrev);
- additionalReviews.innerHTML += `
- <div class="user_reviews_summary_row" id="russian-reviews-row">
- <div class="subtitle column all">Русские:</div>
- <div class="summary column">
- <span class="game_review_summary ${rusClass}">${ruspercent}% из ${formatNumber(rustotalrev)} положительные</span>
- </div>
- </div>
- `;
- document.getElementById('russian-reviews-row').addEventListener('click', function() {
- openModal();
- });
- }
- }
- }
- function openModal() {
- let ofxmodal = document.createElement('div');
- ofxmodal.className = 'ofxmodal';
- ofxmodal.innerHTML = `
- <div class="ofxmodal-content">
- <span class="ofxclose">×</span>
- <button class="refresh-button" id="refresh-reviews">Загрузить актуальные</button>
- <div id="reviews-container"></div>
- </div>
- `;
- document.body.appendChild(ofxmodal);
- ofxmodal.querySelector('.ofxclose').addEventListener('click', function() {
- ofxmodal.style.display = 'none';
- });
- ofxmodal.querySelector('#refresh-reviews').addEventListener('click', function() {
- refreshReviews(ofxmodal);
- });
- ofxmodal.style.display = 'block';
- loadReviews(ofxmodal, 'all');
- }
- function refreshReviews(ofxmodal) {
- ofxmodal.querySelector('#reviews-container').innerHTML = '';
- loadReviews(ofxmodal, 'recent');
- }
- function loadReviews(ofxmodal, filter) {
- fetchRussianReviewsHTML(window.location.pathname.match(/\/app\/(\d+)/)[1], filter, function(html) {
- ofxmodal.querySelector('#reviews-container').innerHTML = html;
- ofxmodal.querySelector('#LoadMoreReviewsall')?.remove();
- ofxmodal.querySelector('#LoadMoreReviewsrecent')?.remove();
- });
- }
- function main() {
- addStyles();
- addRussianIndicators();
- addLoadButton();
- }
- main();
- })();
- }
- // Скрипт для страницы игры (HLTB; получение сведений о времени прохождения) | https://store.steampowered.com/app/*
- if (window.location.pathname.includes('/app/') && scriptsConfig.hltbData) {
- (async function() {
- let hltbBlock = document.createElement('div');
- hltbBlock.style.position = 'absolute';
- hltbBlock.style.top = '232px';
- hltbBlock.style.left = '334px';
- hltbBlock.style.width = '30px';
- hltbBlock.style.height = '30px';
- hltbBlock.style.background = 'rgba(27, 40, 56, 0.95)';
- hltbBlock.style.padding = '15px';
- hltbBlock.style.borderRadius = '4px';
- hltbBlock.style.border = '1px solid #3c3c3c';
- hltbBlock.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
- hltbBlock.style.zIndex = '2';
- hltbBlock.style.fontFamily = 'Arial, sans-serif';
- hltbBlock.style.overflow = 'hidden';
- hltbBlock.style.transition = 'all 0.3s ease';
- let triangle = document.createElement('div');
- triangle.className = 'triangle-down';
- triangle.style.position = 'absolute';
- triangle.style.bottom = '5px';
- triangle.style.left = '50%';
- triangle.style.transform = 'translateX(-50%)';
- triangle.style.width = '0';
- triangle.style.height = '0';
- triangle.style.borderLeft = '5px solid transparent';
- triangle.style.borderRight = '5px solid transparent';
- triangle.style.borderTop = '5px solid #67c1f5';
- triangle.style.cursor = 'pointer';
- hltbBlock.appendChild(triangle);
- let title = document.createElement('div');
- title.style.fontSize = '12px';
- title.style.fontWeight = 'bold';
- title.style.color = '#67c1f5';
- title.style.marginBottom = '10px';
- title.textContent = 'HLTB';
- title.style.cursor = 'pointer';
- hltbBlock.appendChild(title);
- let content = document.createElement('div');
- content.style.fontSize = '14px';
- content.style.color = '#c6d4df';
- content.style.display = 'none';
- content.style.whiteSpace = 'auto';
- content.style.padding = '0 0';
- hltbBlock.appendChild(content);
- const updateHltbPosition = () => {
- const russianIndicators = document.querySelector('#gameHeaderImageCtn > div[style*="position: absolute; top: -10px; left: calc(100% + 10px);"]');
- if (scriptsConfig.gamePage && russianIndicators) {
- hltbBlock.style.top = `${russianIndicators.offsetTop + russianIndicators.offsetHeight + 16}px`;
- } else {
- hltbBlock.style.top = '0px';
- }
- hltbBlock.style.left = '334px';
- };
- const initHltbObservers = () => {
- if (scriptsConfig.gamePage) {
- const indicatorsObserver = new MutationObserver(() => {
- updateHltbPosition();
- });
- const indicators = document.querySelector('#gameHeaderImageCtn > div[style*="position: absolute; top: -10px; left: calc(100% + 10px);"]');
- if (indicators) {
- indicatorsObserver.observe(indicators, {
- attributes: true,
- childList: true,
- subtree: true
- });
- }
- }
- const generalObserver = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- updateHltbPosition();
- }
- });
- });
- generalObserver.observe(document.querySelector('#gameHeaderImageCtn'), {
- childList: true,
- subtree: true
- });
- };
- document.querySelector('#gameHeaderImageCtn').appendChild(hltbBlock);
- initHltbObservers();
- updateHltbPosition();
- const handleClick = async function() {
- if (content.style.display === 'none') {
- hltbBlock.style.transition = 'width 0.3s ease, height 0.3s ease';
- updateHltbPosition();
- await new Promise(resolve => setTimeout(resolve, 50));
- hltbBlock.style.width = '200px';
- hltbBlock.style.height = '40px';
- await new Promise(resolve => setTimeout(resolve, 300));
- content.textContent = 'Ищем в базе...';
- content.style.display = 'block';
- triangle.classList.remove('triangle-down');
- triangle.classList.add('triangle-up');
- triangle.style.borderTop = 'none';
- triangle.style.borderBottom = '5px solid #67c1f5';
- let gameName = getGameName();
- let gameNameNormalized = normalizeGameName(gameName);
- let orangutanFetchUrl = 'https://umadb.ro/hltb/fetch.php';
- let orangutanHltbUrl = "https://howlongtobeat.com";
- try {
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: orangutanFetchUrl,
- onload: resolve,
- onerror: reject
- });
- });
- if (response.status === 200) {
- const key = response.responseText.trim();
- orangutanHltbUrl = "https://howlongtobeat.com" + key;
- } else {
- throw new Error('Failed to fetch key. Status: ' + response.status);
- }
- } catch (error) {
- content.textContent = 'Ошибка при получении ключа.';
- return;
- }
- let chimpQuery = '{"searchType":"games","searchTerms":[' + gameNameNormalized + '],"searchPage":1,"size":20,"searchOptions":{"games":{"userId":0,"platform":"","sortCategory":"popular","rangeCategory":"main","rangeTime":{"min":null,"max":null},"gameplay":{"perspective":"","flow":"","genre":"","difficulty":""},"rangeYear":{"min":"","max":""},"modifier":""},"users":{"sortCategory":"postcount"},"lists":{"sortCategory":"follows"},"filter":"","sort":0,"randomizer":0},"useCache":true}';
- GM_xmlhttpRequest({
- method: "POST",
- url: orangutanHltbUrl,
- data: chimpQuery,
- headers: {
- "Content-Type": "application/json",
- "origin": "https://howlongtobeat.com",
- "referer": "https://howlongtobeat.com/"
- },
- onload: async function(response) {
- let baboonData = {
- count: 0,
- data: []
- };
- if (!response.responseText.includes("<title>HowLongToBeat - 404</title>")) {
- try {
- baboonData = JSON.parse(response.responseText);
- } catch (e) {
- content.textContent = 'Ошибка при обработке данных.';
- return;
- }
- }
- if (baboonData.count === 0 && /[а-яё]/i.test(gameName)) {
- const appId = window.location.pathname.split('/')[2];
- const steamApiUrl = `https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json={"ids": [{"appid": ${appId}}], "context": {"language": "english", "country_code": "US", "steam_realm": 1}, "data_request": {"include_assets": true}}`;
- try {
- const steamResponse = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: steamApiUrl,
- onload: resolve,
- onerror: reject
- });
- });
- if (steamResponse.status === 200) {
- const steamData = JSON.parse(steamResponse.responseText);
- const englishName = steamData.response.store_items[0].name;
- if (englishName) {
- gameName = englishName;
- gameNameNormalized = normalizeGameName(gameName);
- chimpQuery = '{"searchType":"games","searchTerms":[' + gameNameNormalized + '],"searchPage":1,"size":20,"searchOptions":{"games":{"userId":0,"platform":"","sortCategory":"popular","rangeCategory":"main","rangeTime":{"min":null,"max":null},"gameplay":{"perspective":"","flow":"","genre":"","difficulty":""},"rangeYear":{"min":"","max":""},"modifier":""},"users":{"sortCategory":"postcount"},"lists":{"sortCategory":"follows"},"filter":"","sort":0,"randomizer":0},"useCache":true}';
- const secondResponse = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "POST",
- url: orangutanHltbUrl,
- data: chimpQuery,
- headers: {
- "Content-Type": "application/json",
- "origin": "https://howlongtobeat.com",
- "referer": "https://howlongtobeat.com/"
- },
- onload: resolve,
- onerror: reject
- });
- });
- if (secondResponse.status === 200) {
- baboonData = JSON.parse(secondResponse.responseText);
- }
- }
- }
- } catch (error) {
- console.error('Ошибка при запросе к Steam API:', error);
- }
- }
- if (baboonData.count > 0) {
- const matches = findPossibleMatches(gameName, baboonData.data);
- if (matches.length > 0) {
- renderPossibleMatches(matches);
- hltbBlock.style.height = `${content.scrollHeight + 30}px`;
- return;
- }
- }
- renderContent(baboonData.data[0]);
- hltbBlock.style.height = `${content.scrollHeight + 30}px`;
- },
- onerror: function(error) {
- content.textContent = 'Ошибка при запросе к HLTB.';
- },
- ontimeout: function() {
- content.textContent = 'Тайм-аут при запросе к HLTB.';
- },
- timeout: 10000
- });
- } else {
- content.style.display = 'none';
- hltbBlock.style.height = '30px';
- hltbBlock.style.width = '30px';
- triangle.classList.remove('triangle-up');
- triangle.classList.add('triangle-down');
- triangle.style.borderBottom = 'none';
- triangle.style.borderTop = '5px solid #67c1f5';
- }
- };
- title.onclick = handleClick;
- triangle.onclick = handleClick;
- window.addEventListener('resize', updateHltbPosition);
- function normalizeGameName(name) {
- return name
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase()
- .split(/\s+/)
- .map(word => `"${word}"`)
- .join(",");
- }
- function findPossibleMatches(gameName, data) {
- const cleanGameName = gameName
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- return data
- .map(item => {
- const cleanItemName = item.game_name
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- const similarity = calculateSimilarity(cleanGameName, cleanItemName);
- const startsWith = cleanItemName.startsWith(cleanGameName);
- return {
- ...item,
- percentage: similarity,
- startsWith: startsWith
- };
- })
- .filter(item => item.percentage > 50 || item.startsWith)
- .sort((a, b) => {
- if (a.startsWith && !b.startsWith) return -1;
- if (!a.startsWith && b.startsWith) return 1;
- return b.percentage - a.percentage;
- })
- .slice(0, 5);
- }
- function calculateSimilarity(str1, str2) {
- const len = Math.max(str1.length, str2.length);
- if (len === 0) return 100;
- const distance = levenshteinDistance(str1, str2);
- return Math.round(((len - distance) / len) * 100);
- }
- function levenshteinDistance(str1, str2) {
- const m = str1.length;
- const n = str2.length;
- const dp = Array.from({
- length: m + 1
- }, () => Array(n + 1).fill(0));
- for (let i = 0; i <= m; i++) {
- for (let j = 0; j <= n; j++) {
- if (i === 0) {
- dp[i][j] = j;
- } else if (j === 0) {
- dp[i][j] = i;
- } else {
- dp[i][j] = Math.min(
- dp[i - 1][j - 1] + (str1[i - 1] === str2[j - 1] ? 0 : 1),
- dp[i - 1][j] + 1,
- dp[i][j - 1] + 1
- );
- }
- }
- }
- return dp[m][n];
- }
- function getTextWidth(text, font) {
- const canvas = document.createElement('canvas');
- const context = canvas.getContext('2d');
- context.font = font;
- const metrics = context.measureText(text);
- return metrics.width;
- }
- function renderPossibleMatches(matches) {
- content.innerHTML = '';
- const title = document.createElement('div');
- title.textContent = 'Возможные совпадения:';
- title.style.color = '#67c1f5';
- title.style.marginBottom = '10px';
- content.appendChild(title);
- const list = document.createElement('ul');
- list.style.paddingLeft = '15px';
- list.style.marginTop = '5px';
- list.style.marginBottom = '0';
- matches.forEach(match => {
- const li = document.createElement('li');
- li.style.marginBottom = '8px';
- const link = document.createElement('a');
- link.href = '#';
- link.textContent = `${match.game_name} (${match.percentage}%)`;
- link.style.color = '#c6d4df';
- link.style.wordBreak = 'break-word';
- link.style.textDecoration = 'none';
- link.onclick = () => {
- renderContent(match);
- hltbBlock.style.height = `${content.scrollHeight + 30}px`;
- return false;
- };
- li.appendChild(link);
- list.appendChild(li);
- });
- const noMatch = document.createElement('li');
- noMatch.style.marginBottom = '8px';
- const noMatchLink = document.createElement('a');
- noMatchLink.href = '#';
- noMatchLink.textContent = 'Ничего не подходит';
- noMatchLink.style.color = '#c6d4df';
- noMatchLink.style.wordBreak = 'break-word';
- noMatchLink.style.textDecoration = 'none';
- noMatchLink.onclick = () => {
- renderContent(null);
- hltbBlock.style.height = `${content.scrollHeight + 30}px`;
- return false;
- };
- noMatch.appendChild(noMatchLink);
- list.appendChild(noMatch);
- content.appendChild(list);
- let maxWidth = 0;
- content.querySelectorAll('a').forEach(link => {
- const text = link.textContent;
- const font = window.getComputedStyle(link).font;
- const width = getTextWidth(text, font);
- if (width > maxWidth) maxWidth = width;
- });
- hltbBlock.style.width = `${Math.max(maxWidth + 40, 250)}px`;
- }
- function renderContent(entry) {
- content.innerHTML = '';
- if (!entry) {
- content.textContent = 'Игра не найдена в базе HLTB';
- return;
- }
- const titleLink = document.createElement('a');
- titleLink.href = `https://howlongtobeat.com/game/${entry.game_id}`;
- titleLink.target = '_blank';
- titleLink.textContent = entry.game_name || 'Без названия';
- titleLink.style.color = '#67c1f5';
- titleLink.style.wordBreak = 'break-word';
- content.appendChild(titleLink);
- const list = document.createElement('ul');
- list.style.paddingLeft = '15px';
- list.style.marginTop = '5px';
- list.style.marginBottom = '0';
- const times = [{
- label: 'Только сюжет',
- time: entry.comp_main,
- count: entry.comp_main_count
- },
- {
- label: 'Сюжет + доп.',
- time: entry.comp_plus,
- count: entry.comp_plus_count
- },
- {
- label: 'Комплеционист',
- time: entry.comp_100,
- count: entry.comp_100_count
- },
- {
- label: 'Все стили',
- time: entry.comp_all,
- count: entry.comp_all_count
- }
- ];
- times.forEach(time => {
- const li = document.createElement('li');
- li.style.marginBottom = '8px';
- const timeText = time.time ? formatTime(time.time) : "X";
- li.innerHTML = `${time.label}: <span style="color: #fff;">${timeText}</span> (${time.count} чел.)`;
- list.appendChild(li);
- });
- content.appendChild(list);
- let maxWidth = 0;
- content.querySelectorAll('li').forEach(child => {
- const text = child.textContent;
- const font = window.getComputedStyle(child).font;
- const width = getTextWidth(text, font);
- if (width > maxWidth) maxWidth = width;
- });
- hltbBlock.style.width = `${Math.max(maxWidth + 30, 200)}px`;
- hltbBlock.style.whiteSpace = 'nowrap';
- }
- function formatTime(seconds) {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.round((seconds % 3600) / 60);
- if (hours === 0) {
- return `${minutes} м.`;
- } else if (hours + (minutes / 60) >= hours + 0.5) {
- return `${hours + 1} ч.`;
- } else {
- return `${hours} ч.`;
- }
- }
- function getGameName() {
- return document.querySelector('.apphub_AppName').textContent
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .replace(/[’]/g, "'")
- .replace(/[^a-zA-Zа-яёА-ЯЁ0-9 _'\-!]/g, '')
- .trim()
- .toLowerCase();
- }
- if (scriptsConfig.autoExpandHltb) {
- handleClick();
- }
- })();
- }
- // Скрипт для страницы игры (Время друзей & Достижения) | https://store.steampowered.com/app/*
- if (window.location.pathname.includes('/app/') && scriptsConfig.friendsPlaytime) {
- (async function() {
- const statsBlock = document.createElement('div');
- statsBlock.style.position = 'absolute';
- statsBlock.style.top = '0px';
- statsBlock.style.left = '406px';
- statsBlock.style.width = '30px';
- statsBlock.style.height = '30px';
- statsBlock.style.background = 'rgba(27, 40, 56, 0.95)';
- statsBlock.style.padding = '15px';
- statsBlock.style.borderRadius = '4px';
- statsBlock.style.border = '1px solid #3c3c3c';
- statsBlock.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
- statsBlock.style.zIndex = '1';
- statsBlock.style.fontFamily = 'Arial, sans-serif';
- statsBlock.style.overflow = 'hidden';
- statsBlock.style.transition = 'all 0.3s ease';
- const triangle = document.createElement('div');
- triangle.style.position = 'absolute';
- triangle.style.bottom = '5px';
- triangle.style.left = '50%';
- triangle.style.transform = 'translateX(-50%)';
- triangle.style.width = '0';
- triangle.style.height = '0';
- triangle.style.borderLeft = '5px solid transparent';
- triangle.style.borderRight = '5px solid transparent';
- triangle.style.borderTop = '5px solid #67c1f5';
- triangle.style.cursor = 'pointer';
- statsBlock.appendChild(triangle);
- const title = document.createElement('div');
- title.style.display = 'flex';
- title.style.alignItems = 'center';
- title.style.marginBottom = '7px';
- title.style.cursor = 'pointer';
- const combinedImg = document.createElement('div');
- combinedImg.style.width = '29px';
- combinedImg.style.height = '29px';
- combinedImg.style.backgroundImage = 'url(https://gist.githubusercontent.com/0wn3dg0d/9c259eebc40a1e97397ccf3da7ee7bd6/raw/SUEftach.png)';
- combinedImg.style.backgroundSize = 'contain';
- combinedImg.style.backgroundPosition = 'center';
- title.appendChild(combinedImg);
- statsBlock.appendChild(title);
- const content = document.createElement('div');
- content.style.fontSize = '14px';
- content.style.color = '#c6d4df';
- content.style.display = 'none';
- content.style.padding = '0';
- statsBlock.appendChild(content);
- const toggleBlock = async () => {
- if (content.style.display === 'none') {
- statsBlock.style.width = '250px';
- statsBlock.style.height = '60px';
- content.style.display = 'block';
- content.textContent = 'Загрузка...';
- triangle.style.borderTop = 'none';
- triangle.style.borderBottom = '5px solid #67c1f5';
- try {
- const friendsData = await loadFriendsData();
- const achievementsData = await loadAchievementsData();
- content.innerHTML = '';
- const friendsTitle = document.createElement('div');
- friendsTitle.style.fontSize = '12px';
- friendsTitle.style.fontWeight = 'bold';
- friendsTitle.style.color = '#67c1f5';
- friendsTitle.style.marginBottom = '5px';
- friendsTitle.textContent = 'ВРЕМЯ ДРУЗЕЙ';
- content.appendChild(friendsTitle);
- if (friendsData.length > 0) {
- const maxHours = Math.max(...friendsData.map(f => f.hours));
- const minHours = Math.min(...friendsData.map(f => f.hours));
- const avgHours = friendsData.reduce((sum, f) => sum + f.hours, 0) / friendsData.length;
- const maxPlayers = friendsData.filter(f => f.hours === maxHours);
- const maxEl = document.createElement('div');
- maxEl.style.marginBottom = '4px';
- maxEl.innerHTML = `<span style="color: #67c1f5;">Макс:</span> ${maxHours.toFixed(1)} ч.`;
- if (maxPlayers.length > 0) {
- maxEl.innerHTML += ` (${maxPlayers.map(p =>
- `<a href="${p.profile}" target="_blank" style="color: #c6d4df; text-decoration: none;">${p.name}</a>`
- ).join(', ')})`;
- }
- const avgEl = document.createElement('div');
- avgEl.style.marginBottom = '4px';
- avgEl.innerHTML = `<span style="color: #67c1f5;">Среднее:</span> ${avgHours.toFixed(1)} ч. (${friendsData.length} чел.)`;
- const minEl = document.createElement('div');
- minEl.innerHTML = `<span style="color: #67c1f5;">Минимальное:</span> ${minHours.toFixed(1)} ч.`;
- content.append(maxEl, avgEl, minEl);
- } else {
- const noData = document.createElement('div');
- noData.textContent = 'Друзья не играли';
- noData.style.marginBottom = '12px';
- content.appendChild(noData);
- }
- const achTitle = document.createElement('div');
- achTitle.style.fontSize = '12px';
- achTitle.style.fontWeight = 'bold';
- achTitle.style.color = '#67c1f5';
- achTitle.style.margin = '16px 0 5px 0';
- achTitle.textContent = 'ГЛОБАЛЬНЫЕ ДОСТИЖЕНИЯ';
- content.appendChild(achTitle);
- if (achievementsData.hasAchievements) {
- const platinumEl = document.createElement('div');
- platinumEl.style.marginBottom = '4px';
- platinumEl.innerHTML = `<span style="color: #67c1f5;">Платина:</span> ${achievementsData.platinumPercent}%`;
- const averageEl = document.createElement('div');
- averageEl.innerHTML = `<span style="color: #67c1f5;">Средний прогресс:</span> ${achievementsData.averageAdjustedPercent}%`;
- content.append(platinumEl, averageEl);
- } else {
- const noAch = document.createElement('div');
- noAch.textContent = achievementsData.error === 'Нет достижений' ?
- 'Достижений нет' :
- achievementsData.error;
- noAch.style.marginBottom = '12px';
- content.appendChild(noAch);
- }
- statsBlock.style.height = `${content.scrollHeight + 38}px`;
- } catch (error) {
- content.textContent = 'Ошибка загрузки';
- statsBlock.style.height = '60px';
- }
- } else {
- content.style.display = 'none';
- statsBlock.style.height = '30px';
- statsBlock.style.width = '30px';
- triangle.style.borderBottom = 'none';
- triangle.style.borderTop = '5px solid #67c1f5';
- }
- };
- async function loadFriendsData() {
- try {
- const friendsLink = document.querySelector('.recommendation_reasons a[href*="friendsthatplay"]');
- if (!friendsLink) return [];
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: friendsLink.href,
- onload: resolve,
- onerror: reject,
- timeout: 5000
- });
- });
- const parser = new DOMParser();
- const doc = parser.parseFromString(response.responseText, 'text/html');
- return Array.from(doc.querySelectorAll('.friendBlockContent'))
- .map(block => {
- const timeText = block.querySelector('.friendSmallText')?.textContent;
- const hoursMatch = timeText?.match(/(\d+[,.]?\d*)\s*ч/);
- return {
- name: block.firstChild.textContent.trim(),
- hours: hoursMatch ? parseFloat(hoursMatch[1].replace(',', '.')) : 0,
- profile: block.closest('.friendBlock').querySelector('a').href
- };
- })
- .filter(f => f.hours > 0);
- } catch (error) {
- return [];
- }
- }
- async function loadAchievementsData() {
- try {
- const appIdMatch = window.location.pathname.match(/\/app\/(\d+)/);
- if (!appIdMatch) return {
- hasAchievements: false,
- error: 'Не найден App ID'
- };
- const appId = appIdMatch[1];
- const achievementsUrl = `https://steamcommunity.com/stats/${appId}/achievements/`;
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: achievementsUrl,
- onload: resolve,
- onerror: reject,
- timeout: 8000
- });
- });
- if (response.status !== 200) return {
- hasAchievements: false,
- error: 'Ошибка загрузки страницы'
- };
- const parser = new DOMParser();
- const doc = parser.parseFromString(response.responseText, 'text/html');
- if (doc.querySelector('.no_achievements_message')) {
- return {
- hasAchievements: false,
- error: 'Достижения отсутствуют'
- };
- }
- const percentElements = doc.querySelectorAll('.achievePercent');
- if (percentElements.length === 0) return {
- hasAchievements: false,
- error: 'Достижения отсутствуют'
- };
- const percents = Array.from(percentElements)
- .map(el => {
- const text = el.textContent.trim();
- return parseFloat(text.replace('%', '')) || 0;
- })
- .filter(p => p > 0);
- if (percents.length === 0) return {
- hasAchievements: false,
- error: 'Нет данных'
- };
- const maxPercent = Math.max(...percents);
- const minPercent = Math.min(...percents);
- const adjustment = 100 - maxPercent;
- const adjustedPercents = percents.map(p => p + adjustment);
- const averageAdjusted = adjustedPercents.reduce((sum, p) => sum + p, 0) / adjustedPercents.length;
- return {
- hasAchievements: true,
- platinumPercent: (minPercent + adjustment).toFixed(1),
- averageAdjustedPercent: averageAdjusted.toFixed(1),
- };
- } catch (error) {
- return {
- hasAchievements: false,
- error: 'Ошибка соединения'
- };
- }
- }
- title.addEventListener('click', toggleBlock);
- triangle.addEventListener('click', toggleBlock);
- document.querySelector('#gameHeaderImageCtn').appendChild(statsBlock);
- if (scriptsConfig.autoExpandFriends) {
- toggleBlock();
- }
- })();
- }
- // Скрипт для получения дополнительной информации об игре при наведении на неё на странице поиска по каталогу | https://store.steampowered.com/search/
- if (scriptsConfig.catalogInfo && window.location.pathname.includes('/search')) {
- (function() {
- 'use strict';
- const ALEXANDER_API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1";
- const HANNIBAL_WAIT_TIME = 2000;
- const CAESAR_VISIBLE_ELEMENTS_SELECTOR = "a.search_result_row[data-ds-appid]";
- const NAPOLEON_HOVER_ELEMENT_SELECTOR = "a.search_result_row";
- let GENghis_collectedAppIds = new Set();
- let ATTILA_tooltip = null;
- let SALADIN_hoverTimer = null;
- let TAMERLAN_hideTimer = null;
- let RUSSIAN_TRANSLATION_CHECKBOX = null;
- let RUSSIAN_VOICE_CHECKBOX = null;
- let NO_RUSSIAN_CHECKBOX = null;
- const STEAM_TAGS_CACHE_KEY = 'SteamEnhancer_TagsCache_v2';
- const STEAM_TAGS_URL = "https://gist.githubusercontent.com/0wn3dg0d/22a351ff4c65e50a9a8af6da360defad/raw/steamrutagsownd.json";
- async function loadSteamTags() {
- const cached = GM_getValue(STEAM_TAGS_CACHE_KEY, {
- data: null,
- timestamp: 0
- });
- const now = Date.now();
- const CACHE_DURATION = 744 * 60 * 60 * 1000;
- if (cached.data && (now - cached.timestamp) < CACHE_DURATION) {
- return cached.data;
- }
- try {
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: STEAM_TAGS_URL,
- onload: resolve,
- onerror: reject
- });
- });
- if (response.status === 200) {
- const data = JSON.parse(response.responseText);
- GM_setValue(STEAM_TAGS_CACHE_KEY, {
- data: data,
- timestamp: now
- });
- return data;
- }
- } catch (e) {
- console.error('Ошибка загрузки тегов:', e);
- return cached.data || {};
- }
- return {};
- }
- function fetchGameData(appIds) {
- const inputJson = {
- 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: `${ALEXANDER_API_URL}?input_json=${encodeURIComponent(JSON.stringify(inputJson))}`,
- onload: function(response) {
- const data = JSON.parse(response.responseText);
- processGameData(data);
- }
- });
- }
- function processGameData(data) {
- const items = data.response.store_items;
- items.forEach(item => {
- const appId = item.id;
- const gameElement = document.querySelector(`a.search_result_row[data-ds-appid="${appId}"]`);
- if (gameElement) {
- const gameData = {
- is_early_access: item.is_early_access,
- review_count: item.reviews?.summary_filtered?.review_count,
- percent_positive: item.reviews?.summary_filtered?.percent_positive,
- short_description: item.basic_info?.short_description,
- publishers: item.basic_info?.publishers?.map(p => p.name).join(", "),
- developers: item.basic_info?.developers?.map(d => d.name).join(", "),
- franchises: item.basic_info?.franchises?.map(f => f.name).join(", "),
- tagids: item.tagids || [],
- language_support_russian: item.supported_languages?.find(lang => lang.elanguage === 8),
- language_support_english: item.supported_languages?.find(lang => lang.elanguage === 0)
- };
- gameElement.dataset.gameInfo = JSON.stringify(gameData);
- applyRussianLanguageFilter(gameElement);
- }
- });
- }
- function collectAndFetchAppIds() {
- const visibleElements = document.querySelectorAll(CAESAR_VISIBLE_ELEMENTS_SELECTOR);
- const newAppIds = new Set();
- visibleElements.forEach(element => {
- const appId = element.dataset.dsAppid;
- if (!GENghis_collectedAppIds.has(appId)) {
- newAppIds.add(parseInt(appId, 10));
- GENghis_collectedAppIds.add(appId);
- }
- });
- if (newAppIds.size > 0) {
- fetchGameData(newAppIds);
- }
- }
- function handleHover(event) {
- const gameElement = event.target.closest(NAPOLEON_HOVER_ELEMENT_SELECTOR);
- if (gameElement && gameElement.dataset.gameInfo) {
- clearTimeout(SALADIN_hoverTimer);
- clearTimeout(TAMERLAN_hideTimer);
- SALADIN_hoverTimer = setTimeout(() => {
- const gameData = JSON.parse(gameElement.dataset.gameInfo);
- displayGameInfo(gameElement, gameData);
- }, 300);
- } else {
- clearTimeout(SALADIN_hoverTimer);
- clearTimeout(TAMERLAN_hideTimer);
- if (ATTILA_tooltip) {
- ATTILA_tooltip.style.opacity = 0;
- setTimeout(() => {
- ATTILA_tooltip.style.display = 'none';
- }, 300);
- }
- }
- }
- function getReviewClassCatalog(percent, totalReviews) {
- if (totalReviews === 0) return 'catalog-no-reviews';
- if (percent >= 70) return 'catalog-positive';
- if (percent >= 40) return 'catalog-mixed';
- if (percent >= 1) return 'catalog-negative';
- return 'catalog-negative';
- }
- async function getTagNames(tagIds) {
- const tagsData = await loadSteamTags();
- return tagIds.slice(0, 5).map(tagId =>
- tagsData[tagId] || `Тег #${tagId}`
- );
- }
- async function displayGameInfo(element, data) {
- if (!ATTILA_tooltip) {
- ATTILA_tooltip = document.createElement('div');
- ATTILA_tooltip.className = 'custom-tooltip';
- ATTILA_tooltip.innerHTML = '<div class="tooltip-arrow"></div><div class="tooltip-content"></div>';
- document.body.appendChild(ATTILA_tooltip);
- }
- const tooltipContent = ATTILA_tooltip.querySelector('.tooltip-content');
- let languageSupportRussianText = "Отсутствует";
- let languageSupportRussianClass = 'catalog-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>Субтитры: ✔";
- if (languageSupportRussianText === "") languageSupportRussianText = "Отсутствует";
- else languageSupportRussianClass = 'catalog-language-yes';
- }
- let languageSupportEnglishText = "Отсутствует";
- let languageSupportEnglishClass = 'catalog-language-no';
- if (scriptsConfig.toggleEnglishLangInfo && 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>Субтитры: ✔";
- if (languageSupportEnglishText === "") languageSupportEnglishText = "Отсутствует";
- else languageSupportEnglishClass = 'catalog-language-yes';
- }
- const reviewClass = getReviewClassCatalog(data.percent_positive, data.review_count);
- const earlyAccessClass = data.is_early_access ? 'catalog-early-access-yes' : 'catalog-early-access-no';
- const tags = await getTagNames(data.tagids || []);
- const tagsHtml = tags.map(tag =>
- `<div class="custom-tag">${tag}</div>`
- ).join('');
- tooltipContent.innerHTML = `
- <div style="margin-bottom: 0px;"><strong>Издатели:</strong> <span class="${!data.publishers ? 'catalog-no-reviews' : ''}">${data.publishers || "Нет данных"}</span></div>
- <div style="margin-bottom: 0px;"><strong>Разработчики:</strong> <span class="${!data.developers ? 'catalog-no-reviews' : ''}">${data.developers || "Нет данных"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Серия игр:</strong> <span class="${!data.franchises ? 'catalog-no-reviews' : ''}">${data.franchises || "Нет данных"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Отзывы: </strong><span id="reviewCount">${data.review_count || "0"} </span><span class="${reviewClass}">(${data.percent_positive || "0"}% положительных)</span></div>
- <div style="margin-bottom: 10px;"><strong>Ранний доступ:</strong> <span class="${earlyAccessClass}">${data.is_early_access ? "Да" : "Нет"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Русский язык:</strong> <span class="${languageSupportRussianClass}">${languageSupportRussianText}</span></div>
- ${scriptsConfig.toggleEnglishLangInfo ? `<div style="margin-bottom: 10px;"><strong>Английский язык:</strong> <span class="${languageSupportEnglishClass}">${languageSupportEnglishText}</span></div>` : ''}
- <div style="margin-bottom: 10px;"><strong>Метки:</strong><br>
- <div class="custom-tags-container">${tagsHtml}</div></div>
- <div style="margin-bottom: 10px;"><strong>Описание:</strong> <span class="${!data.short_description ? 'catalog-no-reviews' : ''}">${data.short_description || "Нет данных"}</span></div>
- `;
- ATTILA_tooltip.style.display = 'block';
- const rect = element.getBoundingClientRect();
- const tooltipRect = ATTILA_tooltip.getBoundingClientRect();
- ATTILA_tooltip.style.left = `${rect.left + window.scrollX - tooltipRect.width - 4}px`;
- ATTILA_tooltip.style.top = `${rect.top + window.scrollY - 20}px`;
- ATTILA_tooltip.style.opacity = 0;
- ATTILA_tooltip.style.display = 'block';
- setTimeout(() => {
- ATTILA_tooltip.style.opacity = 1;
- }, 10);
- element.addEventListener('mouseleave', () => {
- clearTimeout(TAMERLAN_hideTimer);
- TAMERLAN_hideTimer = setTimeout(() => {
- ATTILA_tooltip.style.opacity = 0;
- setTimeout(() => {
- ATTILA_tooltip.style.display = 'none';
- }, 300);
- }, 200);
- }, {
- once: true
- });
- element.addEventListener('mouseover', () => {
- clearTimeout(TAMERLAN_hideTimer);
- });
- }
- function createRussianLanguageFilterBlock() {
- const filterBlock = document.createElement('div');
- filterBlock.className = 'block search_collapse_block';
- filterBlock.innerHTML = `
- <div data-panel="{"focusable":true,"clickOnActivate":true}" class="block_header labs_block_header">
- <div>Русский перевод</div>
- </div>
- <div class="block_content block_content_inner">
- <div class="tab_filter_control_row" data-param="russian_translation" data-value="__toggle" data-loc="Только текст" data-clientside="0">
- <span data-panel="{"focusable":true,"clickOnActivate":true}" class="tab_filter_control tab_filter_control_include" data-param="russian_translation" data-value="__toggle" data-loc="Только текст" data-clientside="0" data-gpfocus="item">
- <span>
- <span class="tab_filter_control_checkbox"></span>
- <span class="tab_filter_control_label">Только текст</span>
- <span class="tab_filter_control_count" style="display: none;"></span>
- </span>
- </span>
- </div>
- <div class="tab_filter_control_row" data-param="russian_voice" data-value="__toggle" data-loc="Озвучка" data-clientside="0">
- <span data-panel="{"focusable":true,"clickOnActivate":true}" class="tab_filter_control tab_filter_control_include" data-param="russian_voice" data-value="__toggle" data-loc="Озвучка" data-clientside="0" data-gpfocus="item">
- <span>
- <span class="tab_filter_control_checkbox"></span>
- <span class="tab_filter_control_label">Озвучка</span>
- <span class="tab_filter_control_count" style="display: none;"></span>
- </span>
- </span>
- </div>
- <div class="tab_filter_control_row" data-param="no_russian" data-value="__toggle" data-loc="Без перевода" data-clientside="0">
- <span data-panel="{"focusable":true,"clickOnActivate":true}" class="tab_filter_control tab_filter_control_include" data-param="no_russian" data-value="__toggle" data-loc="Без перевода" data-clientside="0" data-gpfocus="item">
- <span>
- <span class="tab_filter_control_checkbox"></span>
- <span class="tab_filter_control_label">Без перевода</span>
- <span class="tab_filter_control_count" style="display: none;"></span>
- </span>
- </span>
- </div>
- </div>
- `;
- const priceBlock = document.querySelector('.block.search_collapse_block[data-collapse-name="price"]');
- priceBlock.parentNode.insertBefore(filterBlock, priceBlock.nextSibling);
- const translationRow = filterBlock.querySelector('[data-param="russian_translation"]');
- const voiceRow = filterBlock.querySelector('[data-param="russian_voice"]');
- const noRussianRow = filterBlock.querySelector('[data-param="no_russian"]');
- [translationRow, voiceRow, noRussianRow].forEach(row => {
- row.addEventListener('click', () => {
- const control = row.querySelector('.tab_filter_control');
- const wasChecked = control.classList.contains('checked');
- [translationRow, voiceRow, noRussianRow].forEach(r => {
- r.querySelector('.tab_filter_control').classList.remove('checked');
- r.classList.remove('checked');
- });
- if (!wasChecked) {
- control.classList.add('checked');
- row.classList.add('checked');
- }
- document.querySelectorAll(CAESAR_VISIBLE_ELEMENTS_SELECTOR).forEach(gameElement => {
- applyRussianLanguageFilter(gameElement);
- });
- });
- });
- }
- function applyRussianLanguageFilter(gameElement) {
- if (!gameElement.dataset.gameInfo) return;
- const gameData = JSON.parse(gameElement.dataset.gameInfo);
- const hasRussianText = gameData.language_support_russian?.supported || gameData.language_support_russian?.subtitles;
- const hasRussianVoice = gameData.language_support_russian?.full_audio;
- const hasAnyRussian = hasRussianText || hasRussianVoice;
- const translationChecked = document.querySelector('[data-param="russian_translation"] .tab_filter_control').classList.contains('checked');
- const voiceChecked = document.querySelector('[data-param="russian_voice"] .tab_filter_control').classList.contains('checked');
- const noRussianChecked = document.querySelector('[data-param="no_russian"] .tab_filter_control').classList.contains('checked');
- if (translationChecked) {
- if (!hasRussianText || hasRussianVoice) animateDisappearance(gameElement);
- else animateAppearance(gameElement);
- } else if (voiceChecked) {
- if (!hasRussianVoice) animateDisappearance(gameElement);
- else animateAppearance(gameElement);
- } else if (noRussianChecked) {
- if (hasAnyRussian) animateDisappearance(gameElement);
- else animateAppearance(gameElement);
- } else {
- animateAppearance(gameElement);
- }
- }
- function animateDisappearance(element) {
- element.style.transition = 'opacity 0.5s ease-out, transform 0.5s ease-out';
- element.style.opacity = '0';
- element.style.transform = 'translateX(-100%)';
- setTimeout(() => {
- element.style.display = 'none';
- }, 500);
- }
- function animateAppearance(element) {
- element.style.display = 'block';
- element.style.opacity = '0';
- element.style.transform = 'translateX(-100%)';
- element.style.transition = 'opacity 0.5s ease-in-out, transform 0.5s ease-in-out';
- setTimeout(() => {
- element.style.opacity = '1';
- element.style.transform = 'translateX(0)';
- }, 0);
- setTimeout(() => {
- element.style.transition = '';
- }, 500);
- }
- function observeNewElements() {
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- collectAndFetchAppIds();
- }
- });
- });
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- }
- function initialize() {
- setTimeout(() => {
- collectAndFetchAppIds();
- observeNewElements();
- document.addEventListener('mouseover', handleHover);
- createRussianLanguageFilterBlock();
- }, HANNIBAL_WAIT_TIME);
- }
- initialize();
- const style = document.createElement('style');
- style.innerHTML = `
- .custom-tooltip {
- position: absolute;
- background: linear-gradient(to bottom, #e3eaef, #c7d5e0);
- color: #30455a;
- padding: 12px;
- border-radius: 0px;
- box-shadow: 0 0 12px #000;
- font-size: 12px;
- max-width: 300px;
- display: none;
- z-index: 1000;
- opacity: 0;
- transition: opacity 0.4s ease-in-out;
- }
- .tooltip-arrow {
- position: absolute;
- right: -9px;
- top: 32px;
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-bottom: 10px solid transparent;
- border-left: 10px solid #E1E8ED;
- }
- .catalog-positive {
- color: #2B80E9;
- }
- .catalog-mixed {
- color: #997a00;
- }
- .catalog-negative {
- color: #E53E3E;
- }
- .catalog-no-reviews {
- color: #929396;
- }
- .catalog-language-yes {
- color: #2B80E9;
- }
- .catalog-language-no {
- color: #E53E3E;
- }
- .catalog-early-access-yes {
- color: #2B80E9;
- }
- .catalog-early-access-no {
- color: #929396;
- }
- .search_result_row {
- transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
- }
- .custom-tags-container {
- display: flex;
- flex-wrap: wrap;
- gap: 3px;
- margin-top: 6px;
- }
- .custom-tag {
- background-color: #96a3ae;
- color: #e3eaef;
- padding: 0 4px;
- border-radius: 2px;
- font-size: 11px;
- line-height: 19px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 200px;
- box-shadow: none;
- margin-bottom: 3px;
- }
- `;
- document.head.appendChild(style);
- })();
- }
- // Скрипт скрытия игр на странице поиска по каталогу | https://store.steampowered.com/search/
- if (scriptsConfig.catalogHider && window.location.pathname.includes('/search')) {
- (function() {
- "use strict";
- function addBeetles() {
- const scarabLinks = document.querySelectorAll("a.search_result_row:not(.ds_ignored):not(.ds_excluded_by_preferences):not(.ds_wishlist):not(.ds_owned)");
- scarabLinks.forEach(link => {
- if (link.querySelector(".my-checkbox")) return;
- const ladybug = document.createElement("input");
- ladybug.type = "checkbox";
- ladybug.className = "my-checkbox";
- ladybug.dataset.aphid = link.dataset.dsAppid;
- link.insertBefore(ladybug, link.firstChild);
- ladybug.addEventListener("change", function() {
- link.style.background = this.checked ? "linear-gradient(to bottom, #381616, #5d1414)" : "";
- });
- });
- }
- function hideSelectedCrickets() {
- const checkedLadybugs = document.querySelectorAll(".my-checkbox:checked");
- checkedLadybugs.forEach(ladybug => {
- const link = document.querySelector(`a[data-ds-appid="${ladybug.dataset.aphid}"]`);
- if (link) {
- link.classList.add("ds_ignored", "ds_flagged");
- ladybug.remove();
- jQuery.ajax({
- url: "https://store.steampowered.com/recommended/ignorerecommendation/",
- type: "POST",
- data: {
- sessionid: g_sessionID,
- appid: ladybug.dataset.aphid,
- remove: 0,
- snr: "1_account_notinterested_",
- },
- success: () => {
- console.log(`Game with appid ${ladybug.dataset.aphid} added to the ignore list`);
- GDynamicStore.InvalidateCache();
- },
- });
- }
- });
- updateAntCounter();
- }
- function removeIgnoredDragonflies() {
- const ignoredGames = document.querySelectorAll("a.search_result_row.ds_ignored, a.search_result_row.ds_excluded_by_preferences,a.search_result_row.ds_wishlist");
- ignoredGames.forEach(game => game.remove());
- updateAntCounter();
- }
- function updateAntCounter() {
- const scarabLinks = document.querySelectorAll("a.search_result_row:not(.ds_ignored):not(.ds_excluded_by_preferences):not(.ds_wishlist):not(.ds_owned)");
- const termiteElement = document.querySelector(".game-counter");
- if (termiteElement) {
- termiteElement.textContent = `Игр осталось: ${scarabLinks.length}`;
- }
- }
- const grasshopperButton = document.createElement("button");
- grasshopperButton.textContent = "Скрыть выбранное";
- grasshopperButton.addEventListener("click", hideSelectedCrickets);
- grasshopperButton.classList.add("my-button", "floating-button");
- document.body.appendChild(grasshopperButton);
- const cockroach = document.createElement("div");
- cockroach.textContent = "Игр осталось: 0";
- cockroach.classList.add("game-counter", "floating-button");
- document.body.appendChild(cockroach);
- GM_addStyle(`
- input[type=checkbox] {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- border: 6px inset rgba(255, 0, 0, 0.8);
- border-radius: 50%;
- width: 42px;
- height: 42px;
- outline: none;
- transition: .15s ease-in-out;
- vertical-align: middle;
- position: absolute;
- left: 0px;
- top: 50%;
- transform: translateY(-50%);
- background-color: rgba(0, 0, 0, 0.0);
- box-shadow: inset 0 0 0 0 rgba(255, 255, 255, 0.5);
- cursor: pointer;
- z-index: 9999;
- }
- input[type=checkbox]:checked {
- background-color: rgba(0, 0, 0, 0.5);
- border-color: #b71c1c;
- box-shadow: inset 0 0 0 12px rgba(255, 0, 0, 0.5);
- }
- input[type=checkbox]:after {
- content: "";
- display: block;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%) scale(0);
- width: 25px;
- height: 25px;
- border-radius: 50%;
- background-color: rgba(0, 0, 0, 0.9);
- opacity: 0.9;
- box-shadow: 0 0 0 0 #b71c1c;
- transition: transform .15s ease-in-out, box-shadow .15s ease-in-out;
- }
- input[type=checkbox]:checked:after {
- transform: translate(-50%, -50%) scale(1);
- box-shadow: 0 0 0 4px #b71c1c;
- }
- .my-button {
- margin-right: 10px;
- padding: 10px 20px;
- border: none;
- border-radius: 50px;
- font-size: 16px;
- font-weight: 700;
- color: #fff;
- background: linear-gradient(to right, #16202D, #1B2838);
- box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
- cursor: pointer;
- font-family: "Roboto", sans-serif;
- margin-top: 245px;
- }
- .my-button:hover {
- background: linear-gradient(to right, #0072ff, #00c6ff);
- box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
- }
- .floating-button {
- position: fixed;
- top: -189px;
- left: 240px;
- z-index: 1000;
- }
- .game-counter {
- margin-right: 10px;
- padding: 10px 20px;
- border: none;
- border-radius: 50px;
- font-size: 16px;
- font-weight: 700;
- color: #fff;
- background: linear-gradient(to right, #16202D, #1B2838);
- box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
- font-family: "Roboto", sans-serif;
- margin-top: 195px;
- }
- `);
- const butterflyObserver = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.type === "childList" && mutation.addedNodes.length) {
- addBeetles();
- removeIgnoredDragonflies();
- updateAntCounter();
- }
- });
- });
- butterflyObserver.observe(document.body, {
- childList: true,
- subtree: true
- });
- addBeetles();
- removeIgnoredDragonflies();
- updateAntCounter();
- })();
- }
- // Скрипт для скрытия новостей в новостном центре: | https://store.steampowered.com/news/
- if (scriptsConfig.newsFilter && window.location.pathname.includes('/news')) {
- (function() {
- 'use strict';
- const stromboliStyle = `
- .etna-checkbox {
- position: absolute;
- top: 50%;
- right: 10px;
- width: 60px;
- height: 60px;
- border-radius: 50%;
- border: 2px solid #66c0f4;
- background-color: rgba(27, 40, 56, 0.7);
- cursor: pointer;
- z-index: 1000;
- transform: translateY(-50%);
- opacity: 0.5;
- }
- .etna-checkbox:checked {
- background-color: rgba(102, 192, 244, 0.8);
- }
- .vesuvius-hide-button {
- position: fixed;
- top: 20px;
- right: 20px;
- padding: 15px 30px;
- background-color: #66c0f4;
- color: #fff;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- z-index: 1000;
- font-size: 18px;
- transition: background-color 0.3s, box-shadow 0.3s;
- }
- .vesuvius-hide-button:hover {
- background-color: #4a90e2;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
- }
- `;
- const krakatoaStyleElement = document.createElement('style');
- krakatoaStyleElement.innerHTML = stromboliStyle;
- document.head.appendChild(krakatoaStyleElement);
- function addEtnaCheckboxes(newsItems) {
- newsItems.forEach(item => {
- const fujiNewsLink = item.querySelector('a.Focusable[href^="/news/app/"]');
- if (fujiNewsLink && !item.querySelector('.etna-checkbox')) {
- const rainierCheckbox = document.createElement('input');
- rainierCheckbox.type = 'checkbox';
- rainierCheckbox.className = 'etna-checkbox';
- rainierCheckbox.addEventListener('click', (event) => {
- event.stopPropagation();
- });
- const kilimanjaroOverlayDiv = item.querySelector('._3HF9tOy_soo1B_odf1XArk');
- if (kilimanjaroOverlayDiv) {
- kilimanjaroOverlayDiv.style.position = 'relative';
- kilimanjaroOverlayDiv.appendChild(rainierCheckbox);
- }
- }
- });
- }
- function addVesuviusHideButton() {
- const vesuviusHideButton = document.createElement('button');
- vesuviusHideButton.className = 'vesuvius-hide-button';
- vesuviusHideButton.textContent = 'Скрыть';
- vesuviusHideButton.onclick = hideSelectedNews;
- document.body.appendChild(vesuviusHideButton);
- }
- function hideSelectedNews() {
- const maunaLoaCheckboxes = document.querySelectorAll('.etna-checkbox:checked');
- maunaLoaCheckboxes.forEach(maunaLoaCheckbox => {
- const newsItem = maunaLoaCheckbox.closest('._398u23KF15gxmeH741ZSyL');
- const fujiNewsLink = newsItem.querySelector('a.Focusable[href^="/news/app/"]').getAttribute('href');
- const shishaldinNewsTitle = newsItem.querySelector('._1M8-Pa3b3WboayCgd5VBJT').textContent;
- const bakerNewsDate = new Date().toISOString();
- const hiddenNews = JSON.parse(localStorage.getItem('hiddenNews') || '[]');
- hiddenNews.push({
- link: fujiNewsLink,
- title: shishaldinNewsTitle,
- date: bakerNewsDate
- });
- localStorage.setItem('hiddenNews', JSON.stringify(hiddenNews));
- newsItem.remove();
- });
- }
- function removeHiddenNews() {
- const hiddenNews = JSON.parse(localStorage.getItem('hiddenNews') || '[]');
- hiddenNews.forEach(news => {
- const newsItem = document.querySelector(`a[href="${news.link}"]`)?.closest('._398u23KF15gxmeH741ZSyL');
- if (newsItem) {
- newsItem.remove();
- }
- });
- }
- function init() {
- removeHiddenNews();
- addEtnaCheckboxes(document.querySelectorAll('._398u23KF15gxmeH741ZSyL'));
- addVesuviusHideButton();
- }
- setTimeout(init, 1000);
- const erebusObserver = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- const newNewsItems = document.querySelectorAll('._398u23KF15gxmeH741ZSyL');
- addEtnaCheckboxes(newNewsItems);
- removeHiddenNews();
- }
- });
- });
- erebusObserver.observe(document.body, {
- childList: true,
- subtree: true
- });
- })();
- }
- // Скрипт для показа годовых и исторических продаж предмета на торговой площадке Steam | https://steamcommunity.com/market/listings/*
- if (scriptsConfig.Kaznachei && window.location.pathname.includes('/market/listings/')) {
- async function fetchSalesInfo() {
- const urlParts = window.location.pathname.split('/');
- const appId = urlParts[3];
- const marketHashName = decodeURIComponent(urlParts[4]);
- const apiUrl = `https://steamcommunity.com/market/pricehistory/?appid=${appId}&market_hash_name=${marketHashName}`;
- try {
- const response = await fetch(apiUrl);
- const data = await response.json();
- if (data.success) {
- const salesData = data.prices;
- const yearlySales = {};
- let totalSales = 0;
- salesData.forEach(sale => {
- const date = sale[0];
- const price = parseFloat(sale[1]);
- const quantity = parseInt(sale[2]);
- const year = date.split(' ')[2];
- const totalForDay = price * quantity;
- if (!yearlySales[year]) {
- yearlySales[year] = {
- total: 0,
- commission: 0,
- developerShare: 0,
- valveShare: 0
- };
- }
- yearlySales[year].total += totalForDay;
- totalSales += totalForDay;
- });
- for (const year in yearlySales) {
- const commission = yearlySales[year].total * 0.13;
- const developerShare = commission * 0.6667;
- const valveShare = commission * 0.3333;
- yearlySales[year].commission = commission;
- yearlySales[year].developerShare = developerShare;
- yearlySales[year].valveShare = valveShare;
- }
- displaySalesInfo(yearlySales, totalSales);
- } else {
- console.error('Не удалось получить информацию о продажах.');
- }
- } catch (error) {
- console.error('Ошибка при получении данных:', error);
- }
- }
- function displaySalesInfo(yearlySales, totalSales) {
- const salesInfoContainer = document.createElement('div');
- salesInfoContainer.style.marginTop = '20px';
- salesInfoContainer.style.padding = '10px';
- salesInfoContainer.style.border = '1px solid #4a4a4a';
- salesInfoContainer.style.backgroundColor = '#1b2838';
- salesInfoContainer.style.borderRadius = '4px';
- salesInfoContainer.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.5)';
- salesInfoContainer.style.color = '#c7d5e0';
- const spoilerHeader = document.createElement('div');
- spoilerHeader.style.cursor = 'pointer';
- spoilerHeader.style.padding = '10px';
- spoilerHeader.style.backgroundColor = '#171a21';
- spoilerHeader.style.borderRadius = '4px 4px 0 0';
- spoilerHeader.style.color = '#c7d5e0';
- spoilerHeader.style.fontWeight = 'bold';
- spoilerHeader.style.fontFamily = '"Motiva Sans", sans-serif';
- spoilerHeader.style.fontSize = '16px';
- spoilerHeader.style.display = 'flex';
- spoilerHeader.style.alignItems = 'center';
- spoilerHeader.style.justifyContent = 'space-between';
- spoilerHeader.innerHTML = 'Информация о продажах <span style="font-size: 12px; transform: rotate(0deg); transition: transform 0.3s ease;">▼</span>';
- spoilerHeader.addEventListener('click', () => {
- const content = spoilerHeader.nextElementSibling;
- content.style.display = content.style.display === 'none' ? 'block' : 'none';
- const arrow = spoilerHeader.querySelector('span');
- arrow.style.transform = content.style.display === 'none' ? 'rotate(0deg)' : 'rotate(180deg)';
- });
- const spoilerContent = document.createElement('div');
- spoilerContent.style.display = 'none';
- spoilerContent.style.padding = '10px';
- spoilerContent.style.borderTop = '1px solid #4a4a4a';
- const yearlySalesTable = document.createElement('table');
- yearlySalesTable.style.width = '100%';
- yearlySalesTable.style.borderCollapse = 'collapse';
- yearlySalesTable.style.marginBottom = '20px';
- yearlySalesTable.style.fontFamily = '"Motiva Sans", sans-serif';
- yearlySalesTable.style.fontSize = '14px';
- const yearlySalesHeader = document.createElement('tr');
- yearlySalesHeader.innerHTML = '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Год</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Сумма продаж за год</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Ушло разработчику</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Ушло Valve</th>';
- yearlySalesTable.appendChild(yearlySalesHeader);
- for (const year in yearlySales) {
- const row = document.createElement('tr');
- row.innerHTML = `<td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${year}</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].total.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].developerShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].valveShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td>`;
- yearlySalesTable.appendChild(row);
- }
- const totalSalesParagraph = document.createElement('p');
- totalSalesParagraph.textContent = `Сумма продаж за всё время: ${totalSales.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
- totalSalesParagraph.style.fontWeight = 'bold';
- totalSalesParagraph.style.fontSize = '16px';
- totalSalesParagraph.style.color = '#c7d5e0';
- totalSalesParagraph.style.fontFamily = '"Motiva Sans", sans-serif';
- const commission = totalSales * 0.13;
- const developerShare = commission * 0.6667;
- const valveShare = commission * 0.3333;
- const developerShareParagraph = document.createElement('p');
- developerShareParagraph.textContent = `Ушло разработчику: ${developerShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
- developerShareParagraph.style.fontSize = '14px';
- developerShareParagraph.style.color = '#c7d5e0';
- developerShareParagraph.style.fontFamily = '"Motiva Sans", sans-serif';
- const valveShareParagraph = document.createElement('p');
- valveShareParagraph.textContent = `Ушло Valve: ${valveShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
- valveShareParagraph.style.fontSize = '14px';
- valveShareParagraph.style.color = '#c7d5e0';
- valveShareParagraph.style.fontFamily = '"Motiva Sans", sans-serif';
- spoilerContent.appendChild(yearlySalesTable);
- spoilerContent.appendChild(totalSalesParagraph);
- spoilerContent.appendChild(developerShareParagraph);
- spoilerContent.appendChild(valveShareParagraph);
- salesInfoContainer.appendChild(spoilerHeader);
- salesInfoContainer.appendChild(spoilerContent);
- const marketHeaderBg = document.querySelector('.market_header_bg');
- if (marketHeaderBg) {
- marketHeaderBg.parentNode.insertBefore(salesInfoContainer, marketHeaderBg.nextSibling);
- }
- }
- setTimeout(fetchSalesInfo, 100);
- }
- // Скрипт для получения дополнительной информации об игре при наведении на неё на странице вашей активности Steam
- if (scriptsConfig.homeInfo && window.location.href.includes('steamcommunity.com') && window.location.pathname.includes('/home')) {
- (function() {
- 'use strict';
- const MOREL_API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1";
- const CHANTERELLE_WAIT_TIME = 2000;
- const PORCINI_VISIBLE_ELEMENTS_SELECTOR = "a[href*='/app/'], a[data-appid]";
- const TRUFFLE_HOVER_ELEMENT_SELECTOR = "a[href*='/app/'], a[data-appid]";
- let SHIITAKE_collectedAppIds = new Set();
- let ENOKI_tooltip = null;
- let MAITAKE_hoverTimer = null;
- let HEN_OF_THE_WOODS_hideTimer = null;
- const MUSHROOM_GAME_DATA = {};
- const STEAM_TAGS_CACHE_KEY = 'SteamEnhancer_TagsCache_v2';
- const STEAM_TAGS_URL = "https://gist.githubusercontent.com/0wn3dg0d/22a351ff4c65e50a9a8af6da360defad/raw/steamrutagsownd.json";
- function fetchGameData(appIds) {
- const inputJson = {
- 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: `${MOREL_API_URL}?input_json=${encodeURIComponent(JSON.stringify(inputJson))}`,
- onload: function(response) {
- const data = JSON.parse(response.responseText);
- processGameData(data);
- }
- });
- }
- function processGameData(data) {
- const items = data.response.store_items;
- items.forEach(item => {
- const appId = item.id;
- MUSHROOM_GAME_DATA[appId] = {
- name: item.name,
- is_early_access: item.is_early_access,
- review_count: item.reviews?.summary_filtered?.review_count,
- percent_positive: item.reviews?.summary_filtered?.percent_positive,
- short_description: item.basic_info?.short_description,
- publishers: item.basic_info?.publishers?.map(p => p.name).join(", "),
- developers: item.basic_info?.developers?.map(d => d.name).join(", "),
- franchises: item.basic_info?.franchises?.map(f => f.name).join(", "),
- tagids: item.tagids || [],
- language_support_russian: item.supported_languages?.find(lang => lang.elanguage === 8),
- language_support_english: item.supported_languages?.find(lang => lang.elanguage === 0),
- release_date: item.release?.steam_release_date ? new Date(item.release.steam_release_date * 1000).toLocaleDateString() : "Нет данных"
- };
- });
- }
- function collectAndFetchAppIds() {
- const visibleElements = document.querySelectorAll(PORCINI_VISIBLE_ELEMENTS_SELECTOR);
- const newAppIds = new Set();
- visibleElements.forEach(element => {
- const appId = element.dataset.appid || element.href.match(/app\/(\d+)/)?.[1];
- if (appId && !SHIITAKE_collectedAppIds.has(appId)) {
- newAppIds.add(parseInt(appId, 10));
- SHIITAKE_collectedAppIds.add(appId);
- }
- });
- if (newAppIds.size > 0) {
- fetchGameData(newAppIds);
- }
- }
- function handleHover(event) {
- const gameElement = event.target.closest(TRUFFLE_HOVER_ELEMENT_SELECTOR);
- if (gameElement) {
- const appId = gameElement.dataset.appid || gameElement.href.match(/app\/(\d+)/)?.[1];
- if (appId && MUSHROOM_GAME_DATA[appId]) {
- clearTimeout(MAITAKE_hoverTimer);
- clearTimeout(HEN_OF_THE_WOODS_hideTimer);
- MAITAKE_hoverTimer = setTimeout(() => {
- displayGameInfo(gameElement, MUSHROOM_GAME_DATA[appId], appId);
- }, 300);
- } else {
- clearTimeout(MAITAKE_hoverTimer);
- clearTimeout(HEN_OF_THE_WOODS_hideTimer);
- if (ENOKI_tooltip) {
- ENOKI_tooltip.style.opacity = 0;
- setTimeout(() => {
- ENOKI_tooltip.style.display = 'none';
- }, 300);
- }
- }
- }
- }
- function getReviewClassCatalog(percent, totalReviews) {
- if (totalReviews === 0) return 'mushroom-no-reviews';
- if (percent >= 70) return 'mushroom-positive';
- if (percent >= 40) return 'mushroom-mixed';
- if (percent >= 1) return 'mushroom-negative';
- return 'mushroom-negative';
- }
- async function loadSteamTags() {
- const cached = GM_getValue(STEAM_TAGS_CACHE_KEY, {
- data: null,
- timestamp: 0
- });
- const now = Date.now();
- const CACHE_DURATION = 744 * 60 * 60 * 1000;
- if (cached.data && (now - cached.timestamp) < CACHE_DURATION) {
- return cached.data;
- }
- try {
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: STEAM_TAGS_URL,
- onload: resolve,
- onerror: reject
- });
- });
- if (response.status === 200) {
- const data = JSON.parse(response.responseText);
- GM_setValue(STEAM_TAGS_CACHE_KEY, {
- data: data,
- timestamp: now
- });
- return data;
- }
- } catch (e) {
- console.error('Ошибка загрузки тегов:', e);
- return cached.data || {};
- }
- return {};
- }
- async function displayGameInfo(element, data, appId) {
- if (!ENOKI_tooltip) {
- ENOKI_tooltip = document.createElement('div');
- ENOKI_tooltip.className = 'mushroom-tooltip';
- ENOKI_tooltip.innerHTML = '<div class="tooltip-arrow"></div><div class="tooltip-content"></div>';
- document.body.appendChild(ENOKI_tooltip);
- }
- const tooltipContent = ENOKI_tooltip.querySelector('.tooltip-content');
- let languageSupportRussianText = "Отсутствует";
- let languageSupportRussianClass = 'mushroom-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>Субтитры: ✔";
- if (languageSupportRussianText === "") languageSupportRussianText = "Отсутствует";
- else languageSupportRussianClass = 'mushroom-language-yes';
- }
- let languageSupportEnglishText = "Отсутствует";
- let languageSupportEnglishClass = 'mushroom-language-no';
- if (scriptsConfig.toggleEnglishLangInfo && 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>Субтитры: ✔";
- if (languageSupportEnglishText === "") languageSupportEnglishText = "Отсутствует";
- else languageSupportEnglishClass = 'mushroom-language-yes';
- }
- const reviewClass = getReviewClassCatalog(data.percent_positive, data.review_count);
- const earlyAccessClass = data.is_early_access ? 'mushroom-early-access-yes' : 'mushroom-early-access-no';
- async function getTagNames(tagIds) {
- const tagsData = await loadSteamTags();
- return tagIds.slice(0, 5).map(tagId =>
- tagsData[tagId] || `Тег #${tagId}`
- );
- }
- const tags = await getTagNames(data.tagids || []);
- const tagsHtml = tags.map(tag =>
- `<div class="mushroom-tag">${tag}</div>`
- ).join('');
- tooltipContent.innerHTML = `
- <div style="margin-bottom: 10px;"><strong>Название:</strong> ${data.name || "Нет данных"}</div>
- <div style="margin-bottom: 10px;"><img src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${appId}/header.jpg" alt="${data.name}" style="width: 50%; height: auto;"></div>
- <div style="margin-bottom: 10px;"><strong>Дата выхода:</strong> ${data.release_date}</div>
- <div style="margin-bottom: 0px;"><strong>Издатели:</strong> <span class="${!data.publishers ? 'mushroom-no-reviews' : ''}">${data.publishers || "Нет данных"}</span></div>
- <div style="margin-bottom: 0px;"><strong>Разработчики:</strong> <span class="${!data.developers ? 'mushroom-no-reviews' : ''}">${data.developers || "Нет данных"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Серия игр:</strong> <span class="${!data.franchises ? 'mushroom-no-reviews' : ''}">${data.franchises || "Нет данных"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Отзывы: </strong><span id="reviewCount">${data.review_count || "0"} </span><span class="${reviewClass}">(${data.percent_positive || "0"}% положительных)</span></div>
- <div style="margin-bottom: 10px;"><strong>Ранний доступ:</strong> <span class="${earlyAccessClass}">${data.is_early_access ? "Да" : "Нет"}</span></div>
- <div style="margin-bottom: 10px;"><strong>Русский язык:</strong> <span class="${languageSupportRussianClass}">${languageSupportRussianText}</span></div>
- ${scriptsConfig.toggleEnglishLangInfo ? `<div style="margin-bottom: 10px;"><strong>Английский язык:</strong> <span class="${languageSupportEnglishClass}">${languageSupportEnglishText}</span></div>` : ''}
- <div style="margin-bottom: 10px;"><strong>Метки:</strong><br>
- <div class="mushroom-tags-container">${tagsHtml}</div></div>
- <div style="margin-bottom: 10px;"><strong>Описание:</strong> <span class="${!data.short_description ? 'mushroom-no-reviews' : ''}">${data.short_description || "Нет данных"}</span></div>
- `;
- ENOKI_tooltip.style.display = 'block';
- const blotterDayElement = document.querySelector('.blotter_day');
- if (blotterDayElement) {
- const blotterRect = blotterDayElement.getBoundingClientRect();
- const tooltipRect = ENOKI_tooltip.getBoundingClientRect();
- ENOKI_tooltip.style.left = `${blotterRect.left - tooltipRect.width - 5}px`;
- ENOKI_tooltip.style.top = `${element.getBoundingClientRect().top + window.scrollY - 35}px`;
- }
- ENOKI_tooltip.style.opacity = 0;
- ENOKI_tooltip.style.display = 'block';
- setTimeout(() => {
- ENOKI_tooltip.style.opacity = 1;
- }, 10);
- element.addEventListener('mouseleave', () => {
- clearTimeout(HEN_OF_THE_WOODS_hideTimer);
- HEN_OF_THE_WOODS_hideTimer = setTimeout(() => {
- ENOKI_tooltip.style.opacity = 0;
- setTimeout(() => {
- ENOKI_tooltip.style.display = 'none';
- }, 300);
- }, 200);
- }, {
- once: true
- });
- element.addEventListener('mouseover', () => {
- clearTimeout(HEN_OF_THE_WOODS_hideTimer);
- });
- }
- function observeNewElements() {
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- collectAndFetchAppIds();
- }
- });
- });
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- }
- function initialize() {
- setTimeout(() => {
- collectAndFetchAppIds();
- observeNewElements();
- document.addEventListener('mouseover', handleHover);
- }, CHANTERELLE_WAIT_TIME);
- }
- initialize();
- const style = document.createElement('style');
- style.innerHTML = `
- .mushroom-tooltip {
- position: absolute;
- background: linear-gradient(to bottom, #e3eaef, #c7d5e0);
- color: #30455a;
- padding: 12px;
- border-radius: 0px;
- box-shadow: 0 0 12px #000;
- font-size: 12px;
- max-width: 300px;
- display: none;
- z-index: 1000;
- opacity: 0;
- transition: opacity 0.4s ease-in-out;
- }
- .tooltip-arrow {
- position: absolute;
- right: -9px;
- top: 32px;
- width: 0;
- height: 0;
- border-top: 10px solid transparent;
- border-bottom: 10px solid transparent;
- border-left: 10px solid #E1E8ED;
- }
- .mushroom-positive {
- color: #2B80E9;
- }
- .mushroom-mixed {
- color: #997a00;
- }
- .mushroom-negative {
- color: #E53E3E;
- }
- .mushroom-no-reviews {
- color: #929396;
- }
- .mushroom-language-yes {
- color: #2B80E9;
- }
- .mushroom-language-no {
- color: #E53E3E;
- }
- .mushroom-early-access-yes {
- color: #2B80E9;
- }
- .mushroom-early-access-no {
- color: #929396;
- }
- .mushroom-tags-container {
- display: flex;
- flex-wrap: wrap;
- gap: 3px;
- margin-top: 6px;
- }
- .mushroom-tag {
- background-color: #96a3ae;
- color: #e3eaef;
- padding: 0 4px;
- border-radius: 2px;
- font-size: 11px;
- line-height: 19px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 200px;
- box-shadow: none;
- margin-bottom: 3px;
- }
- `;
- document.head.appendChild(style);
- })();
- }
- // Скрипт для страницы игры (ZOG; получение сведений о наличии русификаторов) | https://store.steampowered.com/app/*
- if (window.location.pathname.includes('/app/') && scriptsConfig.zogInfo) {
- (async function() {
- const ZOG_CACHE_KEY = 'ZoGRusekiEdrit';
- const ZOG_DATA_URL = 'https://gist.githubusercontent.com/0wn3dg0d/7baa8d9f42b0304fe303e903d44d2ada/raw/zogrusbase.json';
- const zogBlock = document.createElement('div');
- Object.assign(zogBlock.style, {
- position: 'absolute',
- left: '334px',
- width: '30px',
- height: '30px',
- background: 'rgba(27, 40, 56, 0.95)',
- padding: '15px',
- borderRadius: '4px',
- border: '1px solid #3c3c3c',
- boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)',
- zIndex: '2',
- fontFamily: 'Arial, sans-serif',
- overflow: 'hidden',
- transition: 'all 0.3s ease'
- });
- let hltbBlock = null;
- let hltbObserver = null;
- let zogMap = null;
- let zogNameMap = null;
- const updatePosition = () => {
- hltbBlock = document.querySelector('#gameHeaderImageCtn > div[style*="background: rgba(27, 40, 56, 0.95)"]');
- const russianIndicators = document.querySelector('#gameHeaderImageCtn > div[style*="position: absolute; top: -10px; left: calc(100% + 10px);"]');
- if (hltbBlock && scriptsConfig.hltbData) {
- zogBlock.style.top = `${hltbBlock.offsetTop + hltbBlock.offsetHeight + 16}px`;
- } else if (russianIndicators && scriptsConfig.gamePage) {
- zogBlock.style.top = `${russianIndicators.offsetTop + russianIndicators.offsetHeight + 16}px`;
- } else {
- const headerImage = document.querySelector('#gameHeaderImageCtn');
- if (headerImage) {
- zogBlock.style.top = `${0}px`;
- }
- }
- zogBlock.style.left = '334px';
- zogBlock.style.zIndex = '2';
- };
- const initObservers = () => {
- if (scriptsConfig.hltbData) {
- hltbBlock = document.querySelector('#gameHeaderImageCtn > div[style*="background: rgba(27, 40, 56, 0.95)"]');
- if (hltbBlock && !hltbObserver) {
- hltbObserver = new ResizeObserver(updatePosition);
- hltbObserver.observe(hltbBlock);
- hltbBlock.addEventListener('transitionend', updatePosition);
- }
- }
- if (scriptsConfig.gamePage) {
- const russianObserver = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
- updatePosition();
- }
- });
- });
- const indicators = document.querySelector('#gameHeaderImageCtn > div[style*="position: absolute; top: -10px; left: calc(100% + 10px);"]');
- if (indicators) {
- russianObserver.observe(indicators, {
- attributes: true,
- attributeFilter: ['style']
- });
- }
- }
- const generalObserver = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- updatePosition();
- initObservers();
- }
- });
- });
- generalObserver.observe(document.querySelector('#gameHeaderImageCtn'), {
- childList: true,
- subtree: true
- });
- };
- async function loadZogData() {
- const cached = GM_getValue(ZOG_CACHE_KEY);
- const lastUpdated = cached?.lastUpdated || '';
- try {
- const metaResponse = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: 'GET',
- url: 'https://api.github.com/gists/7baa8d9f42b0304fe303e903d44d2ada',
- onload: resolve,
- onerror: reject
- });
- });
- const metaData = JSON.parse(metaResponse.responseText);
- const newLastUpdated = metaData.updated_at;
- if (newLastUpdated === lastUpdated) {
- return cached.data;
- }
- const dataResponse = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: 'GET',
- url: ZOG_DATA_URL,
- onload: resolve,
- onerror: reject
- });
- });
- const newData = JSON.parse(dataResponse.responseText);
- GM_setValue(ZOG_CACHE_KEY, {
- lastUpdated: newLastUpdated,
- data: newData,
- timestamp: Date.now()
- });
- return newData;
- } catch (error) {
- console.error('Ошибка загрузки данных ZOG:', error);
- return cached?.data || [];
- }
- }
- async function initZogData() {
- try {
- const data = await loadZogData();
- zogMap = new Map(data.map(item => [item.app_id, item]));
- zogNameMap = new Map(data.map(item => [
- item.title
- ?.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase(),
- item
- ]));
- } catch (e) {
- console.error('Ошибка инициализации данных ZOG:', e);
- content.textContent = 'Ошибка загрузки базы';
- }
- }
- const title = document.createElement('div');
- Object.assign(title.style, {
- fontSize: '12px',
- fontWeight: 'bold',
- color: '#67c1f5',
- marginBottom: '10px',
- cursor: 'pointer'
- });
- title.textContent = 'ZOG';
- const content = document.createElement('div');
- Object.assign(content.style, {
- display: 'none',
- color: '#c6d4df',
- fontSize: '14px',
- maxWidth: '300px',
- overflowY: 'auto',
- whiteSpace: 'normal',
- lineHeight: '1.4',
- padding: '0 5px'
- });
- const arrow = createArrow();
- zogBlock.append(arrow, title, content);
- document.querySelector('#gameHeaderImageCtn').appendChild(zogBlock);
- initObservers();
- updatePosition();
- await initZogData();
- title.onclick = () => toggleBlock(arrow);
- arrow.onclick = () => toggleBlock(arrow);
- async function toggleBlock(arrowElement) {
- if (content.style.display === 'none') {
- await expandBlock(arrowElement);
- } else {
- collapseBlock(arrowElement);
- }
- }
- async function expandBlock(arrowElement) {
- if (!zogMap || !zogNameMap) {
- console.error('Данные ZOG не инициализированы');
- return;
- }
- zogBlock.style.transition = 'width 0.3s ease, height 0.3s ease';
- zogBlock.style.width = '300px';
- zogBlock.style.height = '40px';
- arrowElement.style.transform = 'translateX(-50%) rotate(180deg)';
- await new Promise(resolve => setTimeout(resolve, 300));
- content.style.display = 'block';
- content.textContent = 'Ищем в базе...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- const appId = getAppId();
- let entry = zogMap.get(appId);
- if (!entry) {
- const gameName = getGameName()
- .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- content.textContent = 'Ищем углубленно...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- entry = zogNameMap.get(gameName);
- if (!entry && /[а-яё]/i.test(gameName)) {
- content.textContent = 'Запрашиваем англ. название...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- const steamApiUrl = `https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json={"ids": [{"appid": ${appId}}], "context": {"language": "english", "country_code": "US", "steam_realm": 1}, "data_request": {"include_assets": true}}`;
- try {
- const steamResponse = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: steamApiUrl,
- onload: resolve,
- onerror: reject
- });
- });
- if (steamResponse.status === 200) {
- const steamData = JSON.parse(steamResponse.responseText);
- const englishName = steamData.response.store_items[0]?.name;
- if (englishName) {
- const cleanEnglishName = englishName
- .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- content.textContent = 'Проверяем англ. название...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- entry = zogNameMap.get(cleanEnglishName);
- if (!entry) {
- content.textContent = 'Проверяем возможные совпадения...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- const possibleMatches = findPossibleMatches(cleanEnglishName, Array.from(zogNameMap.values()));
- if (possibleMatches.length > 0) {
- renderPossibleMatches(possibleMatches);
- zogBlock.style.height = `${content.scrollHeight + 30}px`;
- updatePosition();
- return;
- }
- }
- }
- }
- } catch (error) {
- console.error('Ошибка при запросе к Steam API:', error);
- }
- }
- }
- if (!entry) {
- content.textContent = 'Проверяем возможные совпадения...';
- await new Promise(resolve => requestAnimationFrame(resolve));
- const possibleMatches = findPossibleMatches(getGameName(), Array.from(zogNameMap.values()));
- if (possibleMatches.length > 0) {
- renderPossibleMatches(possibleMatches);
- zogBlock.style.height = `${content.scrollHeight + 30}px`;
- updatePosition();
- return;
- }
- }
- renderContent(entry);
- zogBlock.style.height = `${content.scrollHeight + 30}px`;
- updatePosition();
- }
- function nextFrame() {
- return new Promise(resolve => requestAnimationFrame(resolve));
- }
- function collapseBlock(arrowElement) {
- zogBlock.style.transition = 'width 0.3s ease, height 0.3s ease';
- zogBlock.style.width = '30px';
- zogBlock.style.height = '30px';
- arrowElement.style.transform = 'translateX(-50%) rotate(0deg)';
- content.style.display = 'none';
- updatePosition();
- }
- function renderContent(entry) {
- content.innerHTML = '';
- if (!entry) {
- content.textContent = 'Игра не найдена в базе ZOG';
- return;
- }
- const titleLink = document.createElement('a');
- titleLink.href = `https://www.zoneofgames.ru/games/${entry.id}.html`;
- titleLink.target = '_blank';
- titleLink.textContent = entry.title || 'Без названия';
- titleLink.style.color = '#67c1f5';
- titleLink.style.wordBreak = 'break-word';
- content.appendChild(titleLink);
- const list = document.createElement('ul');
- list.style.paddingLeft = '15px';
- list.style.marginTop = '5px';
- list.style.marginBottom = '0';
- if (entry.localizations?.length > 0) {
- entry.localizations.forEach(loc => {
- const li = document.createElement('li');
- li.style.marginBottom = '8px';
- const link = document.createElement('a');
- link.href = loc.link;
- link.target = '_blank';
- link.textContent = `${loc.name} ${loc.size || ''}`;
- link.style.color = '#c6d4df';
- link.style.wordBreak = 'break-word';
- link.style.textDecoration = 'none';
- li.appendChild(link);
- list.appendChild(li);
- });
- } else {
- list.textContent = 'Русификаторы отсутствуют';
- list.style.color = '#999';
- }
- content.appendChild(list);
- }
- function renderPossibleMatches(matches) {
- content.innerHTML = '';
- const title = document.createElement('div');
- title.textContent = 'Возможные совпадения:';
- title.style.color = '#67c1f5';
- title.style.marginBottom = '10px';
- content.appendChild(title);
- const list = document.createElement('ul');
- list.style.paddingLeft = '15px';
- list.style.marginTop = '5px';
- list.style.marginBottom = '0';
- matches.forEach(match => {
- const li = document.createElement('li');
- li.style.marginBottom = '8px';
- const link = document.createElement('a');
- link.href = `https://www.zoneofgames.ru/games/${match.id}.html`;
- link.target = '_blank';
- link.textContent = `${match.title} (${match.percentage}%)`;
- link.style.color = '#c6d4df';
- link.style.wordBreak = 'break-word';
- link.style.textDecoration = 'none';
- link.onclick = () => {
- renderContent(match);
- zogBlock.style.height = `${content.scrollHeight + 30}px`;
- updatePosition();
- return false;
- };
- li.appendChild(link);
- list.appendChild(li);
- });
- const noMatch = document.createElement('li');
- noMatch.style.marginBottom = '8px';
- const noMatchLink = document.createElement('a');
- noMatchLink.href = '#';
- noMatchLink.textContent = 'Ничего не подходит';
- noMatchLink.style.color = '#c6d4df';
- noMatchLink.style.wordBreak = 'break-word';
- noMatchLink.style.textDecoration = 'none';
- noMatchLink.onclick = () => {
- renderContent(null);
- zogBlock.style.height = `${content.scrollHeight + 30}px`;
- updatePosition();
- return false;
- };
- noMatch.appendChild(noMatchLink);
- list.appendChild(noMatch);
- content.appendChild(list);
- }
- function findPossibleMatches(gameName, data) {
- const cleanGameName = gameName
- .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- return data
- .map(item => {
- const cleanItemName = item.title
- .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- .replace(/[^a-zа-яё0-9 _'\-!]/gi, '')
- .toLowerCase();
- const similarity = calculateSimilarity(cleanGameName, cleanItemName);
- const startsWith = cleanItemName.startsWith(cleanGameName);
- return {
- ...item,
- percentage: similarity,
- startsWith: startsWith
- };
- })
- .filter(item => item.percentage > 50 || item.startsWith)
- .sort((a, b) => {
- if (a.startsWith && !b.startsWith) return -1;
- if (!a.startsWith && b.startsWith) return 1;
- return b.percentage - a.percentage;
- })
- .slice(0, 5);
- }
- function calculateSimilarity(str1, str2) {
- const len = Math.max(str1.length, str2.length);
- if (len === 0) return 100;
- const distance = levenshteinDistance(str1, str2);
- return Math.round(((len - distance) / len) * 100);
- }
- function levenshteinDistance(str1, str2) {
- const m = str1.length;
- const n = str2.length;
- const dp = Array.from({
- length: m + 1
- }, () => Array(n + 1).fill(0));
- for (let i = 0; i <= m; i++) {
- for (let j = 0; j <= n; j++) {
- if (i === 0) {
- dp[i][j] = j;
- } else if (j === 0) {
- dp[i][j] = i;
- } else {
- dp[i][j] = Math.min(
- dp[i - 1][j - 1] + (str1[i - 1] === str2[j - 1] ? 0 : 1),
- dp[i - 1][j] + 1,
- dp[i][j - 1] + 1
- );
- }
- }
- }
- return dp[m][n];
- }
- function createArrow() {
- const arrow = document.createElement('div');
- Object.assign(arrow.style, {
- position: 'absolute',
- bottom: '5px',
- left: '50%',
- width: '0',
- height: '0',
- borderLeft: '5px solid transparent',
- borderRight: '5px solid transparent',
- borderTop: '5px solid #67c1f5',
- cursor: 'pointer',
- transition: 'transform 0.3s ease',
- transform: 'translateX(-50%)'
- });
- return arrow;
- }
- function getAppId() {
- return window.location.pathname.split('/')[2];
- }
- function getGameName() {
- return document.querySelector('.apphub_AppName').textContent
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .replace(/[’]/g, "'")
- .replace(/[^a-zA-Zа-яёА-ЯЁ0-9 _'\-!]/g, '')
- .trim()
- .toLowerCase();
- }
- })();
- }
- // Скрипт для получения уведомлений об изменении дат выхода игр из вашего списка желаемого Steam и показа календаря с датами | https://steamcommunity.com/my/wishlist/
- if (scriptsConfig.wishlistTracker) {
- (function() {
- 'use strict';
- const STORAGE_PREFIX = 'USE_Wishlist_';
- const STORAGE_KEYS = {
- NOTIFICATIONS: STORAGE_PREFIX + 'notifications',
- GAME_DATA: STORAGE_PREFIX + 'gameData',
- LAST_UPDATE: STORAGE_PREFIX + 'lastUpdate'
- };
- const calendarIcon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
- <path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM5 20V10h14v10H5zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/>
- </svg>`;
- const BATCH_SIZE = 200;
- const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
- let notifications = GM_getValue(STORAGE_KEYS.NOTIFICATIONS, []);
- let isPanelOpen = false;
- GM_addStyle(`
- .wishlist-tracker-container {
- position: absolute;
- right: 180px;
- top: 6px;
- z-index: 999;
- }
- .wishlist-tracker-button {
- color: #c6d4df;
- background: rgba(103, 193, 245, 0.1);
- padding: 7px 12px;
- border-radius: 2px;
- cursor: pointer;
- font-size: 13px;
- display: flex;
- align-items: center;
- gap: 4px;
- align-items: center;
- transition: all 0.2s ease;
- }
- .wishlist-tracker-button:hover {
- background: rgba(103, 193, 245, 0.2);
- }
- .notification-badge {
- background: #67c1f5;
- color: #1b2838;
- border-radius: 3px;
- padding: 3px 6px;
- font-size: 14px;
- font-weight: bold;
- margin-left: 8px;
- min-width: 20px;
- text-align: center;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- }
- .status-indicator {
- background: #4a5562;
- color: #c6d4df;
- border-radius: 3px;
- padding: 3px 6px;
- font-size: 12px;
- font-weight: bold;
- margin-left: 5px;
- min-width: 30px;
- text-align: center;
- transition: all 0.3s ease;
- cursor: help;
- }
- .status-ok { background: #4a5562; }
- .status-warning { background: #4a5562; }
- .status-alert1 { background: #665c3a; color: #ffd700; }
- .status-alert2 { background: #804d4d; color: #ffb3b3; }
- .status-critical { background: #e60000; color: #fff; }
- .status-unknown { background: #1b2838; color: #8f98a0; }
- .wishlist-tracker-panel {
- position: fixed;
- right: 132px;
- top: 50px;
- background: #1b2838;
- border: 1px solid #67c1f5;
- width: 500px;
- max-height: 500px;
- min-width: 460px;
- overflow-y: auto;
- z-index: 9999;
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
- display: none;
- }
- .wt-panel-header {
- padding: 15px;
- background: #171a21;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .panel-title {
- font-size: 17px;
- font-weight: 500;
- color: #67c1f5;
- }
- .panel-controls {
- display: flex;
- }
- .panel-controls button {
- background: rgba(30, 45, 60, 0.7);
- border: none;
- color: #c6d4df;
- padding: 8px 14px;
- cursor: pointer;
- margin-left: 5px;
- border-radius: 2px;
- font-weight: 400;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
- transition: background 0.2s ease, box-shadow 0.2s ease;
- }
- .panel-controls button:hover {
- background: rgba(40, 60, 80, 0.9);
- box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
- }
- .panel-controls button:active {
- background: rgba(30, 45, 60, 0.6);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- }
- .calendar-btn {
- padding: 8px 10px !important;
- display: flex;
- align-items: center;
- }
- .wt-notification-item {
- padding: 15px;
- border-bottom: 1px solid #2a475e;
- position: relative;
- transition: opacity 0.3s;
- }
- .notification-content {
- display: flex;
- gap: 15px;
- }
- .notification-image {
- width: 80px;
- height: 45px;
- object-fit: cover;
- }
- .notification-text {
- flex-grow: 1;
- padding-right: 25px;
- }
- .notification-game-title {
- color: #66c0f4;
- font-weight: bold;
- text-decoration: none;
- display: block;
- margin-bottom: 5px;
- }
- .notification-date {
- font-size: 12px;
- color: #8f98a0;
- }
- .notification-dates {
- color: #c6d4df;
- font-size: 13px;
- }
- .wtunread {
- background: rgba(102, 192, 244, 0.15);
- }
- .notification-controls {
- position: absolute;
- right: 10px;
- top: 10px;
- display: flex;
- gap: 8px;
- }
- .notification-control {
- cursor: pointer;
- width: 18px;
- height: 18px;
- opacity: 0.7;
- transition: opacity 0.2s;
- }
- .notification-control:hover {
- opacity: 1;
- }
- .delete-btn {
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #6C7781;
- font-size: 16px;
- font-weight: bold;
- line-height: 1;
- border: none;
- cursor: pointer;
- transition: color 0.2s ease, transform 0.1s ease;
- }
- .delete-btn:hover {
- color: #8F98A0;
- }
- .delete-btn:active {
- color: #800000;
- transform: scale(0.9);
- }
- .loading-indicator {
- color: #67c1f5;
- text-align: center;
- padding: 10px;
- }
- .calendar-wtmodal.active {
- display: flex;
- flex-direction: column;
- }
- .calendar-wtmodal {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 80%;
- height: 80vh;
- background: #1b2838;
- border: 1px solid #67c1f5;
- box-shadow: 0 0 30px rgba(0,0,0,0.7);
- z-index: 100000;
- display: none;
- padding: 20px;
- overflow: hidden;
- }
- .calendar-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 15px;
- border-bottom: 1px solid #2a475e;
- margin-bottom: 15px;
- }
- .calendar-title {
- color: #67c1f5;
- font-size: 25px;
- }
- .calendar-close {
- cursor: pointer;
- color: #8f98a0;
- font-size: 54px;
- padding: 5px;
- }
- .calendar-close:hover {
- color: #67c1f5;
- }
- .calendar-content {
- flex-grow: 1;
- overflow-y: auto;
- padding-right: 10px;
- }
- .calendar-month {
- margin-bottom: 30px;
- }
- .month-header {
- color: #67c1f5;
- font-size: 24px;
- margin-bottom: 15px;
- }
- .calendar-grid {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 2px;
- font-size: 14px;
- font-weight: 500;
- }
- .calendar-grid > div:not(.calendar-day) {
- padding: 10px 0;
- background: #1b2838;
- color: #67c1f5;
- border-bottom: 2px solid #67c1f5;
- text-transform: uppercase;
- text-align: center;
- }
- .calendar-day {
- background: #2a475e;
- min-height: 69px;
- padding: 20px 0 16px 0;
- position: relative;
- display: flex;
- flex-direction: column;
- gap: 3px;
- }
- .day-number {
- position: absolute;
- top: 3px;
- right: 5px;
- color: #8f98a0;
- font-size: 14px;
- z-index: 100003
- }
- .calendar-game {
- display: flex;
- position: relative;
- padding-bottom: 8px;
- align-items: center;
- margin: 5px 0;
- padding: 5px;
- background: rgba(42,71,94,0.5);
- border-radius: 3px;
- transition: background 0.2s;
- text-decoration: none !important;
- color: inherit;
- }
- .calendar-game:not(:last-child)::after {
- content: "";
- position: absolute;
- bottom: -7px;
- left: 0;
- right: 0;
- height: 1px;
- background: linear-gradient(90deg,
- transparent 0%,
- rgba(103, 193, 245, 0.3) 20%,
- rgba(103, 193, 245, 0.4) 50%,
- rgba(103, 193, 245, 0.3) 80%,
- transparent 100%
- );
- margin-top: 8px;
- }
- .calendar-game-approximate .calendar-game-title {
- color: #FFD580 !important;
- opacity: 0.9;
- }
- .calendar-game:hover {
- background: rgba(67, 103, 133, 0.5);
- }
- .calendar-game-image {
- width: 100px;
- height: 45px;
- object-fit: cover;
- margin-right: 10px;
- }
- .calendar-game-title {
- color: #c6d4df;
- font-size: 13px;
- }
- .load-more-months {
- text-align: center;
- padding: 15px;
- }
- .load-more-btn {
- background: rgba(103, 193, 245, 0.1);
- color: #67c1f5;
- border: none;
- padding: 10px 20px;
- cursor: pointer;
- border-radius: 3px;
- }
- .load-more-btn:hover {
- background: rgba(103, 193, 245, 0.2);
- }
- .wt-tooltip {
- display: flex !important;
- position: relative;
- }
- .wt-tooltip .wt-tooltiptext {
- visibility: hidden;
- width: 220px;
- background-color: #171a21;
- color: #c6d4df;
- text-align: center;
- border-radius: 3px;
- padding: 12px;
- position: absolute;
- z-index: 1;
- left: 100%;
- margin-left: 2px;
- opacity: 0;
- transition: opacity 0.3s;
- border: 1px solid #67c1f5;
- }
- .wt-tooltip:hover .wt-tooltiptext {
- visibility: visible;
- opacity: 1;
- }
- `);
- const envelopeIcons = {
- wtunread: `<svg width="20" height="16" viewBox="0 0 32 32" fill="#67c1f5" xmlns="http://www.w3.org/2000/svg">
- <path d="M16.015 18.861l-4.072-3.343-8.862 10.463h25.876l-8.863-10.567-4.079 3.447zM29.926 6.019h-27.815l13.908 11.698 13.907-11.698zM20.705 14.887l9.291 11.084v-18.952l-9.291 7.868zM2.004 7.019v18.952l9.291-11.084-9.291-7.868z"/>
- </svg>`,
- wtread: `<svg width="20" height="16" viewBox="0 0 32 32" fill="#8f98a0" xmlns="http://www.w3.org/2000/svg">
- <path d="M20.139 18.934l9.787-7.999-13.926-9.833-13.89 9.833 9.824 8.032 8.205-0.033zM12.36 19.936l-9.279 10.962h25.876l-9.363-10.9-7.234-0.062zM20.705 19.803l9.291 11.084v-18.952l-9.291 7.868zM2.004 11.935v18.952l9.291-11.084-9.291-7.868z"/>
- </svg>`
- };
- function createNotificationUI() {
- const container = $(`
- <div class="wishlist-tracker-container">
- <div class="wishlist-tracker-button">
- <span>Отслеживание вишлиста</span>
- <div class="status-indicator status-unknown">??</div>
- <div class="notification-badge">${getUnreadCount()}</div>
- </div>
- <div class="wishlist-tracker-panel">
- <div class="wt-panel-header">
- <div class="panel-title">Уведомлений: (${notifications.length})</div>
- <div class="panel-controls">
- <button class="refresh-btn">⟳ Обновить</button>
- <button class="clear-btn">× Очистить</button>
- <button class="calendar-btn">${calendarIcon}</button>
- </div>
- </div>
- </div>
- </div>
- `);
- const panel = container.find('.wishlist-tracker-panel');
- const button = container.find('.wishlist-tracker-button');
- button.click(function(e) {
- e.stopPropagation();
- togglePanel();
- });
- container.find('.refresh-btn').click((e) => {
- e.stopPropagation();
- updateData();
- });
- container.find('.clear-btn').click(() => {
- notifications = [];
- GM_setValue(STORAGE_KEYS.NOTIFICATIONS, notifications);
- updateNotificationPanel();
- updateBadge();
- });
- container.find('.calendar-btn').click((e) => {
- e.stopPropagation();
- showCalendarModal();
- });
- if (window.self === window.top) {
- document.body.appendChild(container[0]);
- }
- updateNotificationPanel();
- $(document).click(() => {
- if (isPanelOpen) {
- panel.hide();
- isPanelOpen = false;
- }
- });
- }
- function showLoadingIndicator() {
- const panel = $('.wishlist-tracker-panel');
- panel.find('.loading-indicator').remove();
- const loading = $(`<div class="loading-indicator">Обновление данных...</div>`);
- panel.append(loading);
- }
- function togglePanel() {
- updateStatusIndicator();
- const panel = $('.wishlist-tracker-panel');
- panel.toggle();
- isPanelOpen = !isPanelOpen;
- if (isPanelOpen) {
- panel.css('display', 'block');
- }
- }
- function updateNotificationPanel() {
- const panel = $('.wishlist-tracker-panel');
- panel.find('.wt-notification-item, .loading-indicator').remove();
- panel.find('.panel-title').text(`Уведомлений: (${notifications.length})`);
- notifications.slice(0, 5000).forEach((notification, index) => {
- const item = $(`
- <div class="wt-notification-item ${notification.wtread ? '' : 'wtunread'}">
- <div class="notification-controls">
- <div class="toggle-wtread-btn notification-control">
- ${notification.wtread ? envelopeIcons.wtread : envelopeIcons.wtunread}
- </div>
- <div class="delete-btn notification-control">X</div>
- </div>
- <div class="notification-content">
- <a href="https://store.steampowered.com/app/${notification.appid}" target="_blank">
- <img src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${notification.appid}/header.jpg"
- class="notification-image">
- </a>
- <div class="notification-text">
- <a href="https://store.steampowered.com/app/${notification.appid}"
- class="notification-game-title" target="_blank">
- ${notification.name}
- </a>
- <div class="notification-dates">
- Дата выхода изменилась:<br>
- <span class="old-date">${formatDate(notification.oldDate)}</span> →
- <span class="new-date">${formatDate(notification.newDate)}</span>
- </div>
- <div class="notification-date">
- Обнаружено: ${new Date(notification.timestamp).toLocaleString()}
- </div>
- </div>
- </div>
- </div>
- `);
- item.find('.delete-btn').click((e) => {
- e.stopPropagation();
- notifications = notifications.filter((_, i) => i !== index);
- GM_setValue(STORAGE_KEYS.NOTIFICATIONS, notifications);
- item.fadeOut(300, () => {
- updateNotificationPanel();
- updateBadge();
- });
- });
- item.find('.toggle-wtread-btn').click((e) => {
- e.stopPropagation();
- notifications[index].wtread = !notifications[index].wtread;
- GM_setValue(STORAGE_KEYS.NOTIFICATIONS, notifications);
- item.toggleClass('wtunread', !notifications[index].wtread);
- item.find('.toggle-wtread-btn').html(notifications[index].wtread ? envelopeIcons.wtread : envelopeIcons.wtunread);
- updateBadge();
- });
- panel.append(item);
- });
- }
- function formatDate(dateInfo) {
- if (!dateInfo || dateInfo.value === 'Не указана') return 'Не указано';
- const value = dateInfo.value;
- const displayType = dateInfo.displayType;
- if (typeof value === 'string' && isNaN(value)) {
- return value;
- }
- const ts = formatTimestamp(value);
- const date = new Date(ts * 1000);
- const monthNames = ["январь", "февраль", "март", "апрель", "май", "июнь",
- "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"
- ];
- const quarter = Math.floor(date.getMonth() / 3) + 1;
- if (displayType) {
- switch (displayType) {
- case 'date_month':
- return `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
- case 'date_quarter':
- return `Q${quarter} ${date.getFullYear()}`;
- case 'date_year':
- return `${date.getFullYear()}`;
- case 'date_full':
- default:
- return date.toLocaleDateString('ru-RU', {
- day: 'numeric',
- month: 'long',
- year: 'numeric'
- });
- }
- }
- return date.toLocaleDateString('ru-RU', {
- day: 'numeric',
- month: 'long',
- year: 'numeric'
- });
- }
- function updateStatusIndicator() {
- const lastUpdate = GM_getValue(STORAGE_KEYS.LAST_UPDATE, 0);
- const hoursPassed = (Date.now() - lastUpdate) / MILLISECONDS_IN_HOUR;
- const indicator = $('.status-indicator');
- const days = Math.floor(hoursPassed / 24);
- const hours = Math.floor(hoursPassed % 24);
- indicator.attr('title', `Данные не обновлялись: ${days} д. и ${hours} ч.`);
- if (!lastUpdate) {
- indicator.text('-').removeClass().addClass('status-indicator status-unknown');
- return;
- }
- if (hoursPassed < 12) {
- indicator.text('OK').removeClass().addClass('status-indicator status-ok');
- } else if (hoursPassed < 24) {
- indicator.text('OK?').removeClass().addClass('status-indicator status-warning');
- } else if (hoursPassed < 48) {
- indicator.text('!').removeClass().addClass('status-indicator status-alert1');
- } else if (hoursPassed < 72) {
- indicator.text('!!').removeClass().addClass('status-indicator status-alert2');
- } else if (hoursPassed < 96) {
- indicator.text('!!!').removeClass().addClass('status-indicator status-critical');
- } else {
- indicator.text('???').removeClass().addClass('status-indicator status-critical');
- }
- }
- function updateBadge() {
- $('.notification-badge').text(getUnreadCount());
- }
- function getUnreadCount() {
- return notifications.filter(n => !n.wtread).length;
- }
- async function fetchWishlistAppIds() {
- return new Promise(resolve => {
- GM_xmlhttpRequest({
- method: 'GET',
- url: 'https://store.steampowered.com/dynamicstore/userdata/',
- onload: function(response) {
- const data = JSON.parse(response.responseText);
- resolve(data.rgWishlist || []);
- }
- });
- });
- }
- async function fetchGameDetails(appIds) {
- const batches = [];
- for (let i = 0; i < appIds.length; i += BATCH_SIZE) {
- batches.push(appIds.slice(i, i + BATCH_SIZE));
- }
- const allDetails = [];
- for (const batch of batches) {
- const details = await fetchBatchDetails(batch);
- allDetails.push(...details);
- await new Promise(resolve => setTimeout(resolve, 1000));
- }
- return allDetails;
- }
- async function fetchBatchDetails(appIds) {
- const requestData = {
- ids: appIds.map(appid => ({
- appid
- })),
- context: {
- language: 'russian',
- country_code: 'RU',
- steam_realm: 1
- },
- data_request: {
- include_release: true,
- include_basic_info: true
- }
- };
- return new Promise(resolve => {
- GM_xmlhttpRequest({
- method: 'GET',
- url: `https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json=${encodeURIComponent(JSON.stringify(requestData))}`,
- onload: function(response) {
- try {
- const data = JSON.parse(response.responseText);
- resolve(data.response?.store_items || []);
- } catch (e) {
- console.error('Error parsing response:', e);
- resolve([]);
- }
- }
- });
- });
- }
- function checkForChanges(currentData) {
- const previousData = GM_getValue(STORAGE_KEYS.GAME_DATA, {});
- const changes = [];
- currentData.forEach(game => {
- const prevGame = previousData[game.appid];
- const currentRelease = getReleaseInfo(game.release);
- const prevRelease = prevGame ? getReleaseInfo(prevGame.rawRelease) : null;
- if (prevGame && (
- currentRelease.date !== prevRelease?.date ||
- currentRelease.type !== prevRelease?.type ||
- currentRelease.displayType !== prevRelease?.displayType
- )) {
- changes.push({
- appid: game.appid,
- name: game.name,
- oldDate: {
- value: prevRelease?.date || 'Не указана',
- displayType: prevRelease?.displayType
- },
- newDate: {
- value: currentRelease.date,
- displayType: currentRelease.displayType
- },
- timestamp: Date.now(),
- wtread: false
- });
- }
- });
- const newGameData = currentData.reduce((acc, game) => {
- acc[game.appid] = {
- name: game.name,
- rawRelease: game.release,
- releaseInfo: getReleaseInfo(game.release)
- };
- return acc;
- }, {});
- GM_setValue(STORAGE_KEYS.GAME_DATA, {
- ...previousData,
- ...newGameData
- });
- if (changes.length > 0) {
- notifications = [...changes, ...notifications];
- GM_setValue(STORAGE_KEYS.NOTIFICATIONS, notifications);
- updateNotificationPanel();
- updateBadge();
- }
- $('.wishlist-tracker-panel .loading-indicator').remove();
- }
- function getReleaseInfo(releaseData) {
- if (!releaseData) return {
- date: 'Не указана',
- type: 'unknown',
- displayType: null
- };
- const displayType = releaseData.coming_soon_display || null;
- if (releaseData.steam_release_date) {
- return {
- date: releaseData.steam_release_date,
- type: 'date',
- displayType: displayType
- };
- }
- if (releaseData.custom_release_date_message) {
- return {
- date: releaseData.custom_release_date_message,
- type: 'custom',
- displayType: null
- };
- }
- return {
- date: 'Не указана',
- type: 'unknown',
- displayType: null
- };
- }
- function formatTimestamp(ts) {
- if (!ts) return ts;
- if (typeof ts === 'string') {
- if (/^\d{4}-\d{2}-\d{2}$/.test(ts)) {
- return Math.floor(new Date(ts).getTime() / 1000);
- }
- return ts;
- }
- return typeof ts === 'number' ? ts : parseInt(ts);
- }
- async function updateData() {
- try {
- showLoadingIndicator();
- const indicator = $('.status-indicator');
- indicator.text('...').removeClass().addClass('status-indicator status-unknown');
- const appIds = await fetchWishlistAppIds();
- const gameDetails = await fetchGameDetails(appIds);
- checkForChanges(gameDetails);
- GM_setValue(STORAGE_KEYS.LAST_UPDATE, Date.now());
- updateStatusIndicator();
- } catch (e) {
- console.error('Update error:', e);
- showErrorIndicator();
- updateStatusIndicator();
- } finally {
- $('.wishlist-tracker-panel .loading-indicator').remove();
- }
- }
- function showErrorIndicator() {
- const panel = $('.wishlist-tracker-panel');
- const error = $(`
- <div class="wt-notification-item" style="color: #ff4747;">
- Ошибка при обновлении данных
- </div>
- `);
- panel.prepend(error);
- setTimeout(() => error.remove(), 5000);
- }
- function showCalendarModal() {
- const gameData = GM_getValue(STORAGE_KEYS.GAME_DATA, {});
- const monthsData = getGamesByMonths(gameData);
- const wtmodal = $(`
- <div class="calendar-wtmodal">
- <div class="calendar-header">
- <div class="calendar-title">Календарь релизов (${monthsData.length} месяцев)</div>
- <div class="calendar-close">×</div>
- </div>
- <div class="calendar-content"></div>
- </div>
- `);
- const clickHandler = (e) => {
- if (!$(e.target).closest('.calendar-wtmodal').length) {
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- wtmodal.remove();
- $(document).off('click', clickHandler);
- }
- };
- wtmodal.find('.calendar-close').click((e) => {
- e.preventDefault();
- e.stopPropagation();
- wtmodal.remove();
- $(document).off('click', clickHandler);
- });
- wtmodal.click(e => e.stopPropagation());
- $(document).on('click', clickHandler);
- $('body').append(wtmodal);
- wtmodal.addClass('active');
- let visibleMonths = 3;
- const renderCalendar = () => {
- const visibleData = monthsData.slice(0, visibleMonths);
- const content = wtmodal.find('.calendar-content').empty();
- visibleData.forEach(({
- month,
- year,
- games
- }) => {
- const monthDate = new Date(year, month);
- const monthName = monthDate.toLocaleString('ru-RU', {
- month: 'long'
- });
- const daysInMonth = new Date(year, month + 1, 0).getDate();
- const firstDay = new Date(year, month, 1).getDay();
- const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1;
- const monthBlock = $(`
- <div class="calendar-month">
- <div class="month-header">${monthName} ${year}</div>
- <div class="calendar-grid"></div>
- </div>
- `);
- const grid = monthBlock.find('.calendar-grid');
- grid.append('<div>Пн</div><div>Вт</div><div>Ср</div><div>Чт</div><div>Пт</div><div>Сб</div><div>Вс</div>');
- for (let i = 0; i < adjustedFirstDay; i++) {
- grid.append('<div class="calendar-day"></div>');
- }
- for (let day = 1; day <= daysInMonth; day++) {
- const dayGames = games.filter(g => {
- const releaseDate = new Date(g.releaseInfo.date * 1000);
- return releaseDate.getDate() === day &&
- releaseDate.getMonth() === month &&
- releaseDate.getFullYear() === year;
- });
- const dayElement = $(`
- <div class="calendar-day">
- <div class="day-number">${day}</div>
- </div>
- `);
- dayGames.sort((a, b) => a.name.localeCompare(b.name)).forEach(game => {
- const isApproximate = ['date_month', 'date_quarter', 'date_year']
- .includes(game.releaseInfo.displayType);
- const gameElement = $(`
- <a href="https://store.steampowered.com/app/${game.appid}"
- target="_blank"
- class="calendar-game ${isApproximate ? 'calendar-game-approximate wt-tooltip' : ''}">
- <img src="https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${game.appid}/header.jpg"
- class="calendar-game-image">
- <div class="calendar-game-title">${game.name}</div>
- ${isApproximate ?
- `<div class="wt-tooltiptext">Приблизительная дата: ${getApproximateDateText(game.releaseInfo)}</div>`
- : ''}
- </a>
- `);
- dayElement.append(gameElement);
- });
- grid.append(dayElement);
- }
- content.append(monthBlock);
- });
- if (visibleMonths < monthsData.length) {
- content.append(`
- <div class="load-more-months">
- <button class="load-more-btn">Показать ещё 3 месяца</button>
- </div>
- `);
- content.find('.load-more-btn').click(() => {
- visibleMonths += 3;
- renderCalendar();
- });
- }
- };
- wtmodal.addClass('active');
- renderCalendar();
- }
- function getGamesByMonths(gameData) {
- const now = new Date();
- const currentYear = now.getFullYear();
- const currentMonth = now.getMonth();
- const games = Object.entries(gameData)
- .map(([appid, game]) => ({
- appid: parseInt(appid),
- ...game,
- releaseDate: game.releaseInfo.date && typeof game.releaseInfo.date === 'number' ?
- new Date(game.releaseInfo.date * 1000) : null
- }))
- .filter(g => g.releaseDate)
- .filter(g => {
- const releaseYear = g.releaseDate.getFullYear();
- const releaseMonth = g.releaseDate.getMonth();
- return (releaseYear > currentYear) ||
- (releaseYear === currentYear && releaseMonth >= currentMonth);
- });
- const monthMap = games.reduce((acc, game) => {
- const year = game.releaseDate.getFullYear();
- const month = game.releaseDate.getMonth();
- const key = `${year}-${month}`;
- if (!acc[key]) {
- acc[key] = {
- year,
- month,
- games: []
- };
- }
- acc[key].games.push(game);
- return acc;
- }, {});
- return Object.values(monthMap)
- .sort((a, b) => a.year === b.year ? a.month - b.month : a.year - b.year);
- }
- function getApproximateDateText(releaseInfo) {
- const date = new Date(releaseInfo.date * 1000);
- const quarter = Math.floor(date.getMonth() / 3) + 1;
- switch (releaseInfo.displayType) {
- case 'date_month':
- return date.toLocaleString('ru-RU', {
- month: 'long',
- year: 'numeric'
- });
- case 'date_quarter':
- return `Q${quarter} ${date.getFullYear()}`;
- case 'date_year':
- return date.getFullYear().toString();
- default:
- return date.toLocaleDateString('ru-RU');
- }
- }
- function initialize() {
- createNotificationUI();
- updateStatusIndicator();
- }
- $(document).ready(initialize);
- })();
- }
- })();