您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Добавляет множество функций для улучшения взаимодействия с магазином и сообществом (Полный список на странице скрипта)
// ==UserScript== // @name Ultimate Steam Enhancer // @namespace https://store.steampowered.com/ // @version 1.9.5 // @description Добавляет множество функций для улучшения взаимодействия с магазином и сообществом (Полный список на странице скрипта) // @author 0wn3df1x // @license MIT // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.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 // @grant GM_deleteValue // @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 // @connect vgtimes.ru // @connect api.digiseller.com // @connect plati.market // @connect digiseller.mycdn.ink // ==/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/* earlyaccdata: true, // Скрипт для страницы игры (Ранний доступ) | https://store.steampowered.com/app/* zogInfo: true, // Скрипт для страницы игры (ZOG; получение сведение о наличии русификаторов) | https://store.steampowered.com/app/* vgtSales: true, // Скрипт для страницы игры (VGT; отображения цен из агрегатора VGTimes) | https://store.steampowered.com/app/* platiSales: true, // Скрипт для страницы игры (Plati; отображение цен с Plati.Market) | 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/app/* if (window.location.pathname.includes('/app/') && scriptsConfig.earlyaccdata) { (function() { 'use strict'; const EAORDATE_STORAGE_KEY = 'USE_EarlyAccess_ordateData'; const EAORDATE_URL = 'https://gist.githubusercontent.com/0wn3dg0d/58a8e35f3d34014ea749a22d02f7e203/raw/eaordate.json'; const CACHE_DURATION = 180 * 24 * 60 * 60 * 1000; // 6 месяцев const getYearForm = (n) => { n = Math.abs(n) % 100; const n1 = n % 10; if (n > 10 && n < 20) return 'лет'; if (n1 === 1) return 'год'; if (n1 >= 2 && n1 <= 4) return 'года'; return 'лет'; }; const getMonthForm = (n) => { n = Math.abs(n) % 100; const n1 = n % 10; if (n > 10 && n < 20) return 'месяцев'; if (n1 === 1) return 'месяц'; if (n1 >= 2 && n1 <= 4) return 'месяца'; return 'месяцев'; }; const parseSteamDate = (dateStr) => { const numericParts = dateStr.split('.'); if (numericParts.length === 3) { const day = parseInt(numericParts[0], 10); const month = parseInt(numericParts[1], 10) - 1; const year = parseInt(numericParts[2], 10); return new Date(year, month, day); } const monthsMap = { 'янв': 0, 'фев': 1, 'мар': 2, 'апр': 3, 'мая': 4, 'июн': 5, 'июл': 6, 'авг': 7, 'сен': 8, 'окт': 9, 'ноя': 10, 'дек': 11 }; const cleanedStr = dateStr.replace(/\./g, ''); const [day, monthNameRaw, year] = cleanedStr.split(' '); const monthName = monthNameRaw.substring(0, 3); return new Date(parseInt(year), monthsMap[monthName], parseInt(day)); }; const fetchOrdateData = async () => { const cachedData = GM_getValue(EAORDATE_STORAGE_KEY, null); if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) { return cachedData.data; } return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: EAORDATE_URL, onload: function(response) { try { const data = JSON.parse(response.responseText); GM_setValue(EAORDATE_STORAGE_KEY, { timestamp: Date.now(), data: data }); resolve(data); } catch (e) { console.error('Error parsing EAOrdate data:', e); resolve(cachedData?.data || []); } }, onerror: () => resolve(cachedData?.data || []) }); }); }; const getAppId = () => { const match = window.location.pathname.match(/\/app\/(\d+)/); return match ? parseInt(match[1]) : null; }; const createInfoBox = (duration, isReleased) => { const infoBox = document.createElement('div'); Object.assign(infoBox.style, { position: 'absolute', top: '-46px', left: '334px', background: isReleased ? 'rgba(103, 193, 245, 0.15)' : 'rgba(245, 166, 35, 0.15)', padding: '6.5px', borderRadius: '3px', border: `1px solid ${isReleased ? '#2A568E' : '#f5a623'}`, fontSize: '12px', color: '#c6d4df', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)', fontFamily: '"Motiva Sans", Arial, sans-serif', zIndex: 3, display: 'inline-block', whiteSpace: 'nowrap' }); let message; if (isReleased) { message = duration ? `Вышла спустя ${duration} раннего доступа` : 'Игра вышла из раннего доступа (срок неизвестен)'; } else { message = `В раннем доступе уже ${duration}`; } infoBox.innerHTML = ` <div style="display: flex; align-items: center; gap: 8px;"> <span style="color: ${isReleased ? '#67c1f5' : '#f5a623'}; font-weight: bold;"> ${isReleased ? '➡️' : '⏳'} </span> <span>${message}</span> </div> `; return infoBox; }; const calculateDuration = (startDate, endDate) => { let diffMonths = (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()); if (endDate.getDate() < startDate.getDate()) diffMonths--; const years = Math.floor(diffMonths / 12); const months = diffMonths % 12; const parts = []; if (years > 0) parts.push(`${years} ${getYearForm(years)}`); if (months > 0) parts.push(`${months} ${getMonthForm(months)}`); return parts.length > 0 ? parts.join(' и ') : 'менее месяца'; }; const main = async () => { const detailsBlock = document.querySelector('#genresAndManufacturer'); const isStillEarlyAccess = !!document.querySelector('#earlyAccessHeader'); if (!detailsBlock) return; const parseDates = () => { const fullText = detailsBlock.textContent; const dates = { earlyDate: null, releaseDate: null }; const earlyMatch = fullText.match(/Дата выпуска в раннем доступе:\s*(\d+\s\S+\s\d{4})/); const releaseMatch = fullText.match(/Дата выхода:\s*(\d+\s\S+\s\d{4})/); if (isStillEarlyAccess && !earlyMatch && releaseMatch) { dates.earlyDate = releaseMatch[1]; } else { if (earlyMatch) dates.earlyDate = earlyMatch[1]; if (releaseMatch) dates.releaseDate = releaseMatch[1]; } return dates; }; const { earlyDate: earlyDateStr, releaseDate: releaseDateStr } = parseDates(); const appid = getAppId(); if (!earlyDateStr && appid) { const ordateData = await fetchOrdateData(); const gameData = ordateData.find(item => item.appid === appid); if (gameData) { try { const ordate = parseSteamDate(gameData.ordate); const releaseDate = releaseDateStr ? parseSteamDate(releaseDateStr) : new Date(); if (ordate >= releaseDate) throw new Error('Invalid date order'); const duration = calculateDuration(ordate, releaseDate); const infoBox = createInfoBox(duration, true); document.querySelector('.game_header_image_ctn')?.appendChild(infoBox); } catch (e) { const infoBox = createInfoBox(null, true); document.querySelector('.game_header_image_ctn')?.appendChild(infoBox); } } return; } const earlyDate = earlyDateStr ? parseSteamDate(earlyDateStr) : isStillEarlyAccess ? parseSteamDate(releaseDateStr) : null; const releaseDate = releaseDateStr ? parseSteamDate(releaseDateStr) : null; if (!earlyDate) return; const endDate = isStillEarlyAccess ? new Date() : releaseDate; if (!endDate) return; try { const duration = calculateDuration(earlyDate, endDate); const infoBox = createInfoBox(duration, !isStillEarlyAccess); document.querySelector('.game_header_image_ctn')?.appendChild(infoBox); } catch (e) { console.error('Early access date calculation error:', e); } }; main(); })(); } // Скрипт для получения дополнительной информации об игре при наведении на неё на странице поиска по каталогу | 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"; const OWNED_APPS_CACHE_KEY = 'SteamEnhancer_OwnedApps'; const USERDATA_URL = 'https://store.steampowered.com/dynamicstore/userdata/'; const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 часа 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 fetchOwnedApps() { const cached = GM_getValue(OWNED_APPS_CACHE_KEY, { data: null, timestamp: 0 }); const now = Date.now(); if (cached.data && (now - cached.timestamp) < CACHE_DURATION) { return cached.data; } try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: USERDATA_URL, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const ownedApps = data.rgOwnedApps || []; GM_setValue(OWNED_APPS_CACHE_KEY, { data: ownedApps, timestamp: now }); return ownedApps; } } 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); } }); } async function processGameData(data) { const ownedApps = await fetchOwnedApps(); const items = data.response.store_items; const dlcFilterActive = document.querySelector('[data-param="your_dlc"] .tab_filter_control')?.classList.contains('checked'); 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), type: item.type, parent_appid: item.related_items?.parent_appid }; gameElement.dataset.gameInfo = JSON.stringify(gameData); applyRussianLanguageFilter(gameElement); if (item.type === 4 && item.related_items?.parent_appid && ownedApps.includes(item.related_items.parent_appid)) { gameElement.classList.add('es_highlighted_dlcforya'); } if (dlcFilterActive) { applyDlcFilter(gameElement, true); } } }); } 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 dlcFilterBlock = document.createElement('div'); dlcFilterBlock.className = 'block search_collapse_block'; dlcFilterBlock.innerHTML = ` <div data-panel="{"focusable":true,"clickOnActivate":true}" class="block_header labs_block_header"> <div>DLC</div> </div> <div class="block_content block_content_inner"> <div class="tab_filter_control_row" data-param="your_dlc" data-value="__toggle" data-loc="Только ваши DLC" data-clientside="0"> <span data-panel="{"focusable":true,"clickOnActivate":true}" class="tab_filter_control tab_filter_control_include" data-param="your_dlc" data-value="__toggle" data-loc="Только ваши DLC" data-clientside="0" data-gpfocus="item"> <span> <span class="tab_filter_control_checkbox"></span> <span class="tab_filter_control_label">Только ваши DLC</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); priceBlock.parentNode.insertBefore(dlcFilterBlock, filterBlock.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"]'); const dlcRow = dlcFilterBlock.querySelector('[data-param="your_dlc"]'); [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); }); }); }); dlcRow.addEventListener('click', () => { const control = dlcRow.querySelector('.tab_filter_control'); const isChecked = !control.classList.contains('checked'); control.classList.toggle('checked'); dlcRow.classList.toggle('checked'); document.querySelectorAll(CAESAR_VISIBLE_ELEMENTS_SELECTOR).forEach(gameElement => { applyDlcFilter(gameElement, isChecked); }); }); } function applyDlcFilter(gameElement, showOnlyDlc) { if (!gameElement.dataset.gameInfo) return; const gameData = JSON.parse(gameElement.dataset.gameInfo); const isDlcForOwnedGame = gameElement.classList.contains('es_highlighted_dlcforya'); if (showOnlyDlc) { if (!isDlcForOwnedGame) { animateDisappearance(gameElement); } else { animateAppearance(gameElement); } } else { animateAppearance(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'); const dlcFilterActive = document.querySelector('[data-param="your_dlc"] .tab_filter_control')?.classList.contains('checked'); if (dlcFilterActive && !gameElement.classList.contains('es_highlighted_dlcforya')) { animateDisappearance(gameElement); return; } 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; } .es_highlighted_dlcforya { background: #822dbf linear-gradient(135deg, rgba(0, 0, 0, 0.70) 10%, rgba(0, 0, 0, 0) 100%) !important; } `; 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); })(); } // Скрипт для страницы игры (Plati; отображение цен с Plati.Market) | https://store.steampowered.com/app/* if (scriptsConfig.platiSales && window.location.pathname.includes('/app/')) { (function() { 'use strict'; // --- Конфигурация PlatiSearch (PS) --- const PS_API_BASE_URL = 'https://api.digiseller.com/api/products/search2'; const PS_SUGGEST_API_URL = 'https://plati.market/api/suggest.ashx'; const PS_IMAGE_DOMAIN = 'digiseller.mycdn.ink'; const PS_RESULTS_PER_PAGE_CHECK = 1; const PS_DEFAULT_SORT_MODE = 2; // Relevance const PS_SUGGEST_DEBOUNCE_MS = 300; const PS_FILTER_DEBOUNCE_MS = 500; const PS_FILTER_STORAGE_PREFIX = 'platiSalesFilter_v1_'; const PS_EXCLUSION_STORAGE_KEY = 'platiSalesExclusions_v1_'; const PS_LAST_SORT_STORAGE_KEY = 'platiSalesLastSort_v1_'; const PS_CURRENCY_STORAGE_KEY = 'platiSalesCurrency_v1_'; const PS_FILTER_PANEL_WIDTH = 230; const PS_EXCLUSION_PANEL_WIDTH = 250; const PS_SIDE_PANEL_HORIZONTAL_PADDING = 20; const PS_CONTENT_PADDING_BUFFER = 15; const PS_CONTENT_PADDING_LEFT = PS_FILTER_PANEL_WIDTH + PS_SIDE_PANEL_HORIZONTAL_PADDING + PS_CONTENT_PADDING_BUFFER; const PS_CONTENT_PADDING_RIGHT = PS_EXCLUSION_PANEL_WIDTH + PS_SIDE_PANEL_HORIZONTAL_PADDING + PS_CONTENT_PADDING_BUFFER; const PS_HEADER_APPROX_HEIGHT = 65; const PS_TOP_OFFSET_FOR_SIDE_PANELS = PS_HEADER_APPROX_HEIGHT + 25; const PS_BOTTOM_OFFSET_FOR_SIDE_PANELS = 20; const PS_ADV_SORT_CONTAINER_WIDTH = 230; const NEW_ITEM_THRESHOLD_DAYS = 7; // --- Глобальные переменные --- let ps_currentResults = []; let ps_currentSort = GM_getValue(PS_LAST_SORT_STORAGE_KEY, { field: 'relevance', direction: 'asc' }); let ps_currentCurrency = GM_getValue(PS_CURRENCY_STORAGE_KEY, 'RUR'); let ps_firstSortClick = {}; ['price', 'sales', 'relevance', 'name', 'date_create', 'discount', 'seller_rating', 'review_ratio', 'good_reviews', 'bad_reviews', 'returns'].forEach(field => { ps_firstSortClick[field] = ps_currentSort.field !== field; }); let ps_exclusionKeywords = GM_getValue(PS_EXCLUSION_STORAGE_KEY, []); let ps_currentFilters = ps_loadFilters(); let ps_suggestDebounceTimeout; let ps_filterDebounceTimeout; let ps_advSortMenuTimeout; // --- DOM Элементы --- let ps_modal, ps_closeBtn, ps_searchInput, ps_searchBtn, ps_sortPriceBtn, ps_sortSalesBtn, ps_advSortBtnContainer, ps_advSortBtn, ps_advSortMenu, ps_currencySelect, ps_resetSortBtn; let ps_resultsContainer, ps_resultsDiv, ps_statusDiv, ps_excludeInput, ps_addExcludeBtn, ps_exclusionTagsDiv; let ps_suggestionsDiv; let ps_filtersPanel; let ps_filterPriceMin, ps_filterPriceMax, ps_filterSalesMin, ps_filterSalesMax, ps_filterRatingMin, ps_filterRatingMax; let ps_filterHideBadReviews, ps_filterHideReturns, ps_filterOnlyDiscount; let ps_filterDateSelect; let ps_resetAllFiltersBtn; let ps_exclusionTagsListDiv; // --- Описания сортировок --- const ps_advancedSorts = { 'price': { name: 'По цене', defaultDir: 'asc' }, 'sales': { name: 'По продажам', defaultDir: 'desc'}, 'relevance': { name: 'По релевантности', defaultDir: 'asc' }, 'name': { name: 'По названию', defaultDir: 'asc' }, 'date_create': { name: 'По дате добавления', defaultDir: 'desc' }, 'discount': { name: 'По % в скид. системе', defaultDir: 'desc' }, 'seller_rating':{ name: 'По рейтингу продавца', defaultDir: 'desc' }, 'review_ratio': { name: 'По соотношению отзывов', defaultDir: 'desc' }, 'good_reviews': { name: 'По кол-ву хор. отзывов', defaultDir: 'desc' }, 'bad_reviews': { name: 'По кол-ву плох. отзывов', defaultDir: 'asc' }, 'returns': { name: 'По кол-ву возвратов', defaultDir: 'asc' } }; const ps_advSortOrder = ['name', 'date_create', 'discount', 'seller_rating', 'review_ratio', 'good_reviews', 'bad_reviews', 'returns']; const ps_dateFilterOptions = { 'all': 'За все время', '1d': 'За сутки', '2d': 'За 2 дня', '1w': 'За неделю', '1m': 'За месяц', '6m': 'За полгода', '1y': 'За год', '5y': 'За 5 лет', '10y': 'За 10 лет', }; // --- Вспомогательные функции --- function formatPrice(priceStr) { if (!priceStr) return 0; return parseFloat(String(priceStr).replace(/[^\d,.]/g, '').replace(',', '.')) || 0; } function formatSales(salesStr) { if (!salesStr) return 0; return parseInt(String(salesStr).replace(/\D/g, ''), 10) || 0; } function parseSellerRating(ratingStr) { if (!ratingStr) return 0; return parseFloat(String(ratingStr).replace(',', '.')) || 0; } function calculateReviewRatio(item) { const good = parseInt(item.cnt_good_responses || '0', 10); const bad = parseInt(item.cnt_bad_responses || '0', 10); const total = good + bad; return total > 0 ? (good / total) : -1; } function parseDate(dateStr) { if (!dateStr) return 0; const parts = dateStr.split(' '); if (parts.length !== 2) return 0; const dateParts = parts[0].split('.'); const timeParts = parts[1].split(':'); if (dateParts.length !== 3 || timeParts.length !== 3) return 0; try { return new Date(Date.UTC(dateParts[2], dateParts[1] - 1, dateParts[0], timeParts[0], timeParts[1], timeParts[2])).getTime(); } catch (e) { return 0; } } function formatDateString(timestamp) { if (!timestamp || timestamp === 0) return 'N/A'; try { const date = new Date(timestamp); const day = String(date.getUTCDate()).padStart(2, '0'); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const year = String(date.getUTCFullYear()).slice(-2); return `${day}.${month}.${year}`; } catch (e) { return 'N/A'; } } function getPriceInSelectedCurrency(item, currency) { let price = 0; switch (currency) { case 'USD': price = formatPrice(item.price_usd); break; case 'EUR': price = formatPrice(item.price_eur); break; case 'UAH': price = formatPrice(item.price_uah); break; case 'RUR': default: price = formatPrice(item.price_rur); break; } if (price <= 0 && currency !== 'RUR') price = formatPrice(item.price_rur); if (price <= 0 && currency !== 'USD') price = formatPrice(item.price_usd); if (price <= 0 && currency !== 'EUR') price = formatPrice(item.price_eur); return price > 0 ? price : Infinity; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function getSteamGameName() { const appNameElement = document.querySelector('#appHubAppName'); return appNameElement ? appNameElement.textContent.trim() : ''; } // --- Создание UI --- function createPlatiModal() { const existingModal = document.querySelector('#platiSearchModal'); if (existingModal) existingModal.remove(); ps_modal = document.createElement('div'); ps_modal.id = 'platiSearchModal'; const container = document.createElement('div'); container.id = 'platiSearchContainer'; const header = document.createElement('div'); header.id = 'platiSearchHeader'; const searchInputContainer = document.createElement('div'); searchInputContainer.className = 'platiSearchInputContainer'; ps_searchInput = document.createElement('input'); ps_searchInput.id = 'platiSearchInput'; ps_searchInput.type = 'text'; ps_searchInput.placeholder = 'Введите название игры или товара...'; ps_searchInput.autocomplete = 'off'; ps_searchInput.onkeydown = (e) => { if (e.key === 'Enter') ps_triggerSearch(); }; ps_searchInput.oninput = () => { clearTimeout(ps_suggestDebounceTimeout); ps_suggestDebounceTimeout = setTimeout(() => ps_fetchSuggestions(ps_searchInput.value), PS_SUGGEST_DEBOUNCE_MS); }; ps_searchInput.onblur = () => { setTimeout(() => { if (ps_suggestionsDiv) ps_suggestionsDiv.style.display = 'none'; }, 150); }; ps_suggestionsDiv = document.createElement('div'); ps_suggestionsDiv.id = 'platiSearchSuggestions'; searchInputContainer.appendChild(ps_searchInput); searchInputContainer.appendChild(ps_suggestionsDiv); header.appendChild(searchInputContainer); ps_searchBtn = document.createElement('button'); ps_searchBtn.textContent = 'Найти'; ps_searchBtn.id = 'platiSearchGoBtn'; ps_searchBtn.className = 'platiSearchBtn'; ps_searchBtn.onclick = ps_triggerSearch; header.appendChild(ps_searchBtn); ps_resetSortBtn = document.createElement('button'); ps_resetSortBtn.id = 'platiResetSortBtn'; ps_resetSortBtn.className = 'platiSearchBtn'; ps_resetSortBtn.title = 'Сбросить сортировку (Релевантность)'; ps_resetSortBtn.innerHTML = `<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6s-2.69 6-6 6s-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8s-3.58-8-8-8Z"/></svg>`; ps_resetSortBtn.onclick = () => ps_resetSort(true); header.appendChild(ps_resetSortBtn); ps_sortPriceBtn = document.createElement('button'); ps_sortPriceBtn.className = 'platiSearchBtn sortBtn'; ps_sortPriceBtn.dataset.sort = 'price'; ps_sortPriceBtn.onclick = () => ps_handleSort('price'); header.appendChild(ps_sortPriceBtn); ps_sortSalesBtn = document.createElement('button'); ps_sortSalesBtn.className = 'platiSearchBtn sortBtn'; ps_sortSalesBtn.dataset.sort = 'sales'; ps_sortSalesBtn.onclick = () => ps_handleSort('sales'); header.appendChild(ps_sortSalesBtn); ps_advSortBtnContainer = document.createElement('div'); ps_advSortBtnContainer.id = 'platiSearchAdvSortBtnContainer'; ps_advSortBtn = document.createElement('button'); ps_advSortBtn.id = 'platiSearchAdvSortBtn'; ps_advSortBtn.className = 'platiSearchBtn sortBtn'; ps_advSortBtnContainer.appendChild(ps_advSortBtn); ps_advSortMenu = document.createElement('div'); ps_advSortMenu.id = 'platiSearchAdvSortMenu'; ps_advSortOrder.forEach(key => { const sortInfo = ps_advancedSorts[key]; const menuItem = document.createElement('div'); menuItem.className = 'platiSearchSortMenuItem'; menuItem.dataset.sort = key; menuItem.innerHTML = `${sortInfo.name} <span class="sortArrow"></span>`; // Стрелка добавится в update menuItem.onclick = () => ps_handleSort(key); ps_advSortMenu.appendChild(menuItem); }); ps_advSortBtnContainer.appendChild(ps_advSortMenu); header.appendChild(ps_advSortBtnContainer); ps_currencySelect = document.createElement('select'); ps_currencySelect.id = 'platiSearchCurrencySelect'; ['RUR', 'USD', 'EUR', 'UAH'].forEach(curr => { const option = document.createElement('option'); option.value = curr; option.textContent = curr; if (curr === ps_currentCurrency) option.selected = true; ps_currencySelect.appendChild(option); }); ps_currencySelect.onchange = ps_handleCurrencyChange; header.appendChild(ps_currencySelect); container.appendChild(header); ps_resultsContainer = document.createElement('div'); ps_resultsContainer.id = 'platiSearchResultsContainer'; ps_statusDiv = document.createElement('div'); ps_statusDiv.id = 'platiSearchResultsStatus'; ps_resultsDiv = document.createElement('div'); ps_resultsDiv.id = 'platiSearchResults'; ps_resultsContainer.appendChild(ps_statusDiv); ps_resultsContainer.appendChild(ps_resultsDiv); container.appendChild(ps_resultsContainer); ps_modal.appendChild(container); ps_filtersPanel = document.createElement('div'); ps_filtersPanel.id = 'platiSearchFiltersPanel'; ps_filtersPanel.innerHTML = ` <div class="filterGroup"> <h4>Цена (${ps_currentCurrency}) ${ps_createResetButtonHTML('price')}</h4> <div class="filterRangeInputs"> <input type="number" id="psFilterPriceMin" placeholder="от" min="0"> <input type="number" id="psFilterPriceMax" placeholder="до" min="0"> </div> </div> <div class="filterGroup"> <h4>Продажи ${ps_createResetButtonHTML('sales')}</h4> <div class="filterRangeInputs"> <input type="number" id="psFilterSalesMin" placeholder="от" min="0"> <input type="number" id="psFilterSalesMax" placeholder="до" min="0"> </div> </div> <div class="filterGroup"> <h4>Рейтинг продавца ${ps_createResetButtonHTML('rating')}</h4> <div class="filterRangeInputs"> <input type="number" id="psFilterRatingMin" placeholder="от" step="0.1" min="0"> <input type="number" id="psFilterRatingMax" placeholder="до" step="0.1" min="0"> </div> </div> <div class="filterGroup"> <h4>Опции ${ps_createResetButtonHTML('options')}</h4> <div class="filterCheckbox"> <label><input type="checkbox" id="psFilterHideBadReviews"> Скрыть с плох. отзывами</label> </div> <div class="filterCheckbox"> <label><input type="checkbox" id="psFilterHideReturns"> Скрыть с возвратами</label> </div> <div class="filterCheckbox"> <label><input type="checkbox" id="psFilterOnlyDiscount"> Участие в скидках</label> </div> </div> <div class="filterGroup"> <h4>Дата добавления ${ps_createResetButtonHTML('date')}</h4> <div class="filterSelect"> <select id="psFilterDateSelect"> ${Object.entries(ps_dateFilterOptions).map(([key, text]) => `<option value="${key}">${text}</option>`).join('')} </select> </div> </div> <button id="psResetAllFiltersBtn" class="platiSearchBtn">Сбросить все фильтры</button> `; ps_modal.appendChild(ps_filtersPanel); ps_exclusionTagsDiv = document.createElement('div'); ps_exclusionTagsDiv.id = 'platiSearchExclusionTags'; const exclusionInputGroup = document.createElement('div'); exclusionInputGroup.className = 'exclusionInputGroup'; ps_excludeInput = document.createElement('input'); ps_excludeInput.type = 'text'; ps_excludeInput.id = 'platiSearchExcludeInput'; ps_excludeInput.placeholder = 'Исключить слово'; ps_excludeInput.onkeydown = (e) => { if (e.key === 'Enter') ps_addFilterKeyword(); }; ps_addExcludeBtn = document.createElement('button'); ps_addExcludeBtn.id = 'platiSearchAddExcludeBtn'; ps_addExcludeBtn.innerHTML = `<svg viewBox="0 0 20 20"><path d="M10 2.5a.75.75 0 0 1 .75.75v6h6a.75.75 0 0 1 0 1.5h-6v6a.75.75 0 0 1-1.5 0v-6h-6a.75.75 0 0 1 0-1.5h6v-6a.75.75 0 0 1 .75-.75Z" /></svg>`; ps_addExcludeBtn.onclick = ps_addFilterKeyword; exclusionInputGroup.appendChild(ps_excludeInput); exclusionInputGroup.appendChild(ps_addExcludeBtn); ps_exclusionTagsDiv.appendChild(exclusionInputGroup); ps_exclusionTagsListDiv = document.createElement('div'); ps_exclusionTagsListDiv.id = 'platiExclusionTagsList'; ps_exclusionTagsDiv.appendChild(ps_exclusionTagsListDiv); ps_modal.appendChild(ps_exclusionTagsDiv); ps_closeBtn = document.createElement('button'); ps_closeBtn.id = 'platiSearchCloseBtn'; ps_closeBtn.innerHTML = '×'; ps_closeBtn.onclick = hidePlatiModal; ps_modal.appendChild(ps_closeBtn); document.body.appendChild(ps_modal); // Назначение переменных элементам UI ps_filterPriceMin = document.getElementById('psFilterPriceMin'); ps_filterPriceMax = document.getElementById('psFilterPriceMax'); ps_filterSalesMin = document.getElementById('psFilterSalesMin'); ps_filterSalesMax = document.getElementById('psFilterSalesMax'); ps_filterRatingMin = document.getElementById('psFilterRatingMin'); ps_filterRatingMax = document.getElementById('psFilterRatingMax'); ps_filterHideBadReviews = document.getElementById('psFilterHideBadReviews'); ps_filterHideReturns = document.getElementById('psFilterHideReturns'); ps_filterOnlyDiscount = document.getElementById('psFilterOnlyDiscount'); ps_filterDateSelect = document.getElementById('psFilterDateSelect'); ps_resetAllFiltersBtn = document.getElementById('psResetAllFiltersBtn'); ps_addFilterEventListeners(); applyLoadedFiltersToUI(); ps_updateSortButtonsState(); // Устанавливаем начальное состояние кнопок сортировки function handleEsc(event) { if (event.key === 'Escape') hidePlatiModal(); } document.addEventListener('keydown', handleEsc); ps_modal._escHandler = handleEsc; } function ps_createResetButtonHTML(filterKey) { return `<button class="filterResetBtn" title="Сбросить фильтр" data-filter-key="${filterKey}"><svg viewBox="0 0 24 24"><path d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"></path></svg></button>`; } // --- Управление Модальным Окном --- function showPlatiModal() { if (!ps_modal) createPlatiModal(); const gameName = getSteamGameName(); if (gameName && !ps_searchInput.value) { ps_searchInput.value = gameName; } document.body.style.overflow = 'hidden'; ps_modal.style.display = 'block'; ps_modal.scrollTop = 0; ps_renderExclusionTags(); applyLoadedFiltersToUI(); ps_updateFilterPlaceholders(); ps_updateSortButtonsState(); requestAnimationFrame(() => { const header = document.getElementById('platiSearchHeader'); const headerRect = header ? header.getBoundingClientRect() : { bottom: PS_TOP_OFFSET_FOR_SIDE_PANELS }; const newTopOffset = headerRect.bottom + 5; const availableHeight = `calc(100vh - ${newTopOffset}px - ${PS_BOTTOM_OFFSET_FOR_SIDE_PANELS}px)`; if (ps_filtersPanel) { ps_filtersPanel.style.top = `${newTopOffset}px`; ps_filtersPanel.style.maxHeight = availableHeight;} if (ps_exclusionTagsDiv) { ps_exclusionTagsDiv.style.top = `${newTopOffset}px`; ps_exclusionTagsDiv.style.maxHeight = availableHeight; } }); if (ps_searchInput.value.trim()) { ps_triggerSearch(); } else { ps_updateStatus('Введите запрос для поиска.'); } } function hidePlatiModal() { if (ps_modal) { ps_modal.style.display = 'none'; if (ps_suggestionsDiv) ps_suggestionsDiv.style.display = 'none'; if (ps_modal._escHandler) { document.removeEventListener('keydown', ps_modal._escHandler); delete ps_modal._escHandler; } } document.body.style.overflow = ''; } // --- Обновление статуса --- function ps_updateStatus(message, isLoading = false) { if (ps_statusDiv) { ps_statusDiv.innerHTML = message + (isLoading ? ' <span class="spinner"></span>' : ''); ps_statusDiv.style.display = 'block'; if(ps_currentResults.length === 0 && message && !isLoading) { ps_resultsDiv.innerHTML = ''; } } } // --- Запуск поиска --- function ps_triggerSearch() { const query = ps_searchInput.value.trim(); if (ps_suggestionsDiv) ps_suggestionsDiv.style.display = 'none'; if (!query) { ps_updateStatus('Пожалуйста, введите запрос.'); ps_currentResults = []; ps_renderResults(); return; } ps_currentResults = []; ps_resetSort(false); // Сброс на релевантность без рендера applyLoadedFiltersToUI(); ps_renderResults(); ps_updateStatus('Получение общего количества товаров...', true); ps_fetchTotalCount(query); } // --- Функции подсказок --- function ps_fetchSuggestions(query) { const trimmedQuery = query.trim(); if (trimmedQuery.length < 2) { if (ps_suggestionsDiv) { ps_suggestionsDiv.innerHTML = ''; ps_suggestionsDiv.style.display = 'none'; } return; } const params = new URLSearchParams({ q: trimmedQuery, v: 2 }); try { if (typeof plang !== 'undefined') params.append('lang', plang); if (typeof clientgeo !== 'undefined') params.append('geo', clientgeo); } catch (e) { console.warn("PlatiSearch: Could not get plang/clientgeo for suggestions."); } GM_xmlhttpRequest({ method: "GET", url: `${PS_SUGGEST_API_URL}?${params.toString()}`, timeout: 5000, onload: function(response) { try { ps_renderSuggestions(JSON.parse(response.responseText)); } catch (e) { if (ps_suggestionsDiv) { ps_suggestionsDiv.innerHTML = ''; ps_suggestionsDiv.style.display = 'none'; } } }, onerror: function(error) { if (ps_suggestionsDiv) { ps_suggestionsDiv.innerHTML = ''; ps_suggestionsDiv.style.display = 'none'; } }, ontimeout: function() { if (ps_suggestionsDiv) { ps_suggestionsDiv.innerHTML = ''; ps_suggestionsDiv.style.display = 'none'; } } }); } function ps_renderSuggestions(suggestions) { if (!ps_suggestionsDiv) return; if (!suggestions || !Array.isArray(suggestions) || suggestions.length === 0) { ps_suggestionsDiv.innerHTML = ''; ps_suggestionsDiv.style.display = 'none'; return; } ps_suggestionsDiv.innerHTML = ''; let addedSuggestions = 0; suggestions.forEach(suggestion => { if (suggestion && suggestion.name && (suggestion.type === "Товары" || suggestion.type === "Search" || suggestion.type === "Игры")) { const item = document.createElement('div'); item.className = 'suggestionItem'; item.textContent = suggestion.name; item.onmousedown = (e) => { e.preventDefault(); ps_searchInput.value = suggestion.name; ps_suggestionsDiv.style.display = 'none'; ps_triggerSearch(); }; ps_suggestionsDiv.appendChild(item); addedSuggestions++; } }); ps_suggestionsDiv.style.display = addedSuggestions > 0 ? 'block' : 'none'; } // --- Запросы API --- function ps_fetchTotalCount(query) { // При запросе количества всегда используем сортировку по релевантности (дефолт API) const params = new URLSearchParams({ query: query, searchmode: 10, sortmode: PS_DEFAULT_SORT_MODE, pagesize: PS_RESULTS_PER_PAGE_CHECK, pagenum: 1, owner: 1, details: 1, checkhidesales: 1, host: 'plati.market' }); GM_xmlhttpRequest({ method: "GET", url: `${PS_API_BASE_URL}?${params.toString()}`, timeout: 15000, responseType: 'json', onload: function(response) { if (response.status >= 200 && response.status < 400 && response.response) { const data = response.response; if (data?.result?.total > 0) { const total = data.result.total; ps_updateStatus(`Найдено ${total} товаров. Загрузка...`, true); // Загружаем все результаты с дефолтной сортировкой API (релевантность) ps_fetchAllResults(query, total, PS_DEFAULT_SORT_MODE); } else { ps_updateStatus(`По запросу "${query}" ничего не найдено.`); ps_currentResults = []; ps_renderResults(); ps_updateFilterPlaceholders(); ps_applyFilters(); } } else { ps_updateStatus(`Ошибка получения общего количества товаров (Статус: ${response.status})`); } }, onerror: function(error) { ps_updateStatus('Ошибка сети при получении общего количества товаров.'); }, ontimeout: function() { ps_updateStatus('Время ожидания ответа от сервера (количество) истекло.'); } }); } function ps_fetchAllResults(query, total, sortMode) { const MAX_PAGE_SIZE = 1000; const effectivePageSize = Math.min(total, MAX_PAGE_SIZE); if (total > MAX_PAGE_SIZE) ps_updateStatus(`Найдено ${total} товаров. Загрузка первых ${MAX_PAGE_SIZE}...`, true); const params = new URLSearchParams({ query: query, searchmode: 10, sortmode: sortMode, pagesize: effectivePageSize, pagenum: 1, owner: 1, details: 1, checkhidesales: 1, host: 'plati.market' }); GM_xmlhttpRequest({ method: "GET", url: `${PS_API_BASE_URL}?${params.toString()}`, timeout: 90000, responseType: 'json', onload: function(response) { // Проверяем, открыто ли еще модальное окно if (!document.body.contains(ps_modal)) return; if (response.status >= 200 && response.status < 400 && response.response) { const data = response.response; if (data?.items?.item && Array.isArray(data.items.item)) { ps_currentResults = data.items.item.map((item, index) => ({ ...item, originalIndex: index })); const loadedCount = ps_currentResults.length; ps_updateStatus(`Загружено ${loadedCount}${total > loadedCount ? ` из ${total}` : ''} товаров.`); // После загрузки применяем ТЕКУЩУЮ выбранную пользователем сортировку ps_applySort(ps_currentSort.field, ps_currentSort.direction); ps_renderResults(); ps_updateFilterPlaceholders(); ps_applyFilters(); } else { ps_updateStatus(`Ошибка загрузки товаров: неверный формат ответа API.`); ps_currentResults = []; ps_renderResults(); ps_updateFilterPlaceholders(); ps_applyFilters(); } } else { ps_updateStatus(`Ошибка загрузки товаров (Статус: ${response.status})`); } }, onerror: function(error) { if (document.body.contains(ps_modal)) ps_updateStatus('Ошибка сети при загрузке товаров.'); }, ontimeout: function() { if (document.body.contains(ps_modal)) ps_updateStatus('Время ожидания ответа от сервера (товары) истекло.'); } }); } // --- Сортировка --- function ps_handleSort(field) { let newDirection; const sortInfo = ps_advancedSorts[field]; if (!sortInfo) return; // Неизвестное поле сортировки // Определяем текущее направление из сохраненного состояния let currentDir = (ps_currentSort.field === field) ? ps_currentSort.direction : sortInfo.defaultDir; // Определяем новое направление if (ps_firstSortClick[field] || ps_currentSort.field !== field) { // Если это первый клик по этому полю или клик по новому полю, используем дефолтное направление newDirection = sortInfo.defaultDir; } else { // Иначе инвертируем текущее направление newDirection = currentDir === 'desc' ? 'asc' : 'desc'; } // Обновляем флаги первого клика Object.keys(ps_firstSortClick).forEach(key => { ps_firstSortClick[key] = (key !== field); }); ps_firstSortClick[field] = false; // Устанавливаем, что это уже не первый клик // Сохраняем новое состояние ps_currentSort.field = field; ps_currentSort.direction = newDirection; GM_setValue(PS_LAST_SORT_STORAGE_KEY, ps_currentSort); // Применяем сортировку КЛИЕНТСКИ к уже загруженным данным ps_applySort(field, newDirection); ps_renderResults(); // Перерисовываем с новой сортировкой ps_updateSortButtonsState(); // Обновляем UI кнопок } function ps_updateSortButtonsState() { const activeField = ps_currentSort.field; const activeDirection = ps_currentSort.direction; // Основные кнопки (Цена, Продажи) $(ps_sortPriceBtn).add(ps_sortSalesBtn).each(function() { const $btn = $(this); const btnField = $btn.data('sort'); const baseText = (btnField === 'price') ? 'Цена' : 'Продажи'; if (btnField === activeField) { const arrow = activeDirection === 'asc' ? ' ▲' : ' ▼'; $btn.addClass('active').text(baseText + arrow).attr('data-dir', activeDirection); } else { const defaultDir = ps_advancedSorts[btnField].defaultDir; const defaultArrow = defaultDir === 'asc' ? ' ▲' : ' ▼'; $btn.removeClass('active').text(baseText + defaultArrow).attr('data-dir', defaultDir); } }); // Кнопка и меню доп. сортировки let advBtnText = 'Доп. сорт.'; const $advButton = $(ps_advSortBtn); const isAdvSortActive = ps_advancedSorts[activeField] && activeField !== 'price' && activeField !== 'sales' && activeField !== 'relevance'; if (isAdvSortActive) { $advButton.addClass('active'); const arrow = activeDirection === 'asc' ? ' ▲' : ' ▼'; advBtnText = `${ps_advancedSorts[activeField].name}${arrow}`; } else { $advButton.removeClass('active'); } $advButton.text(advBtnText); // Пункты меню $('#platiSearchAdvSortMenu .platiSearchSortMenuItem').each(function() { const $item = $(this); const itemField = $item.data('sort'); const baseText = ps_advancedSorts[itemField].name; if (itemField === activeField) { const arrow = activeDirection === 'asc' ? ' ▲' : ' ▼'; $item.addClass('active').html(`${baseText} <span class="sortArrow">${arrow}</span>`).attr('data-dir', activeDirection); } else { const defaultDir = ps_advancedSorts[itemField].defaultDir; const defaultArrow = defaultDir === 'asc' ? ' ▲' : ' ▼'; $item.removeClass('active').html(`${baseText} <span class="sortArrow">${defaultArrow}</span>`).attr('data-dir', defaultDir); } }); // Кнопка сброса показывает релевантность if (activeField === 'relevance') { $(ps_resetSortBtn).addClass('active'); } else { $(ps_resetSortBtn).removeClass('active'); } } function ps_resetSort(render = true) { ps_currentSort = { field: 'relevance', direction: 'asc' }; // Релевантность - это исходный порядок API ps_firstSortClick = { price: true, sales: true, relevance: false, name: true, date_create: true, discount: true, seller_rating: true, review_ratio: true, good_reviews: true, bad_reviews: true, returns: true }; GM_setValue(PS_LAST_SORT_STORAGE_KEY, ps_currentSort); ps_updateSortButtonsState(); if (render) { ps_applySort(ps_currentSort.field, ps_currentSort.direction); ps_renderResults(); } } function ps_applySort(field, direction) { const dirMultiplier = direction === 'asc' ? 1 : -1; const selectedCurrency = ps_currencySelect ? ps_currencySelect.value.toUpperCase() : 'RUR'; ps_currentResults.sort((a, b) => { let valA, valB; const nameA = (a.name || '').toLowerCase(); const nameB = (b.name || '').toLowerCase(); const finalPriceA = getPriceInSelectedCurrency(a, selectedCurrency); const finalPriceB = getPriceInSelectedCurrency(b, selectedCurrency); let comparisonResult = 0; switch (field) { case 'price': valA = finalPriceA; valB = finalPriceB; break; case 'sales': valA = formatSales(a.cnt_sell); valB = formatSales(b.cnt_sell); break; case 'name': comparisonResult = nameA.localeCompare(nameB) * dirMultiplier; break; case 'date_create': valA = parseDate(a.date_create); valB = parseDate(b.date_create); break; case 'discount': valA = parseInt(a.discount || '0', 10); valB = parseInt(b.discount || '0', 10); break; case 'seller_rating': valA = parseSellerRating(a.seller_rating); valB = parseSellerRating(b.seller_rating); break; case 'review_ratio': valA = calculateReviewRatio(a); valB = calculateReviewRatio(b); break; case 'good_reviews': valA = parseInt(a.cnt_good_responses || '0', 10); valB = parseInt(b.cnt_good_responses || '0', 10); break; case 'bad_reviews': valA = parseInt(a.cnt_bad_responses || '0', 10); valB = parseInt(b.cnt_bad_responses || '0', 10); break; case 'returns': valA = parseInt(a.cnt_return || '0', 10); valB = parseInt(b.cnt_return || '0', 10); break; case 'relevance': valA = a.originalIndex; valB = b.originalIndex; break; default: return 0; } if (field !== 'name') { const fallbackAsc = Infinity; const fallbackDesc = -Infinity; if (valA === null || valA === undefined || isNaN(valA) || valA === Infinity || valA === -Infinity) valA = direction === 'asc' ? fallbackAsc : fallbackDesc; if (valB === null || valB === undefined || isNaN(valB) || valB === Infinity || valB === -Infinity) valB = direction === 'asc' ? fallbackAsc : fallbackDesc; if (valA < valB) comparisonResult = -1; else if (valA > valB) comparisonResult = 1; else comparisonResult = 0; comparisonResult *= dirMultiplier; } // Вторичная сортировка для стабильности if (comparisonResult === 0) { if (field !== 'name') { let nameCompare = nameA.localeCompare(nameB); if (nameCompare !== 0) return nameCompare; } if (field !== 'price') { if (finalPriceA < finalPriceB) return -1; if (finalPriceA > finalPriceB) return 1; } if (field !== 'relevance') { return a.originalIndex - b.originalIndex; } } return comparisonResult; }); } // --- Управление Фильтрами --- function ps_getFilterStorageKey(key) { return `${PS_FILTER_STORAGE_PREFIX}${key}`; } function ps_loadFilters() { const defaults = { priceMin: '', priceMax: '', salesMin: '', salesMax: '', ratingMin: '', ratingMax: '', hideBadReviews: false, hideReturns: false, onlyDiscount: false, date: 'all' }; let loaded = {}; for (const key in defaults) { loaded[key] = GM_getValue(ps_getFilterStorageKey(key), defaults[key]); } return loaded; } function ps_saveFilter(key, value) { ps_currentFilters[key] = value; GM_setValue(ps_getFilterStorageKey(key), value); } function applyLoadedFiltersToUI() { if (!ps_filtersPanel) return; ps_filterPriceMin.value = ps_currentFilters.priceMin; ps_filterPriceMax.value = ps_currentFilters.priceMax; ps_filterSalesMin.value = ps_currentFilters.salesMin; ps_filterSalesMax.value = ps_currentFilters.salesMax; ps_filterRatingMin.value = ps_currentFilters.ratingMin; ps_filterRatingMax.value = ps_currentFilters.ratingMax; ps_filterHideBadReviews.checked = ps_currentFilters.hideBadReviews; ps_filterHideReturns.checked = ps_currentFilters.hideReturns; ps_filterOnlyDiscount.checked = ps_currentFilters.onlyDiscount; ps_filterDateSelect.value = ps_currentFilters.date; const priceHeader = ps_filtersPanel.querySelector('.filterGroup h4'); if (priceHeader && priceHeader.textContent.includes('Цена')) { priceHeader.innerHTML = `Цена (${ps_currentCurrency}) ${ps_createResetButtonHTML('price')}`; const resetButton = priceHeader.querySelector('.filterResetBtn'); if (resetButton) resetButton.onclick = ps_handleFilterReset; } } function ps_addFilterEventListeners() { if (!ps_filtersPanel) return; const debouncedApply = debounce(ps_applyFilters, PS_FILTER_DEBOUNCE_MS); ps_filterPriceMin.addEventListener('input', (e) => { ps_saveFilter('priceMin', e.target.value); debouncedApply(); }); ps_filterPriceMax.addEventListener('input', (e) => { ps_saveFilter('priceMax', e.target.value); debouncedApply(); }); ps_filterSalesMin.addEventListener('input', (e) => { ps_saveFilter('salesMin', e.target.value); debouncedApply(); }); ps_filterSalesMax.addEventListener('input', (e) => { ps_saveFilter('salesMax', e.target.value); debouncedApply(); }); ps_filterRatingMin.addEventListener('input', (e) => { ps_saveFilter('ratingMin', e.target.value); debouncedApply(); }); ps_filterRatingMax.addEventListener('input', (e) => { ps_saveFilter('ratingMax', e.target.value); debouncedApply(); }); ps_filterHideBadReviews.addEventListener('change', (e) => { ps_saveFilter('hideBadReviews', e.target.checked); ps_applyFilters(); }); ps_filterHideReturns.addEventListener('change', (e) => { ps_saveFilter('hideReturns', e.target.checked); ps_applyFilters(); }); ps_filterOnlyDiscount.addEventListener('change', (e) => { ps_saveFilter('onlyDiscount', e.target.checked); ps_applyFilters(); }); ps_filterDateSelect.addEventListener('change', (e) => { ps_saveFilter('date', e.target.value); ps_applyFilters(); }); ps_resetAllFiltersBtn.addEventListener('click', () => ps_resetAllFilters(true)); ps_filtersPanel.querySelectorAll('.filterResetBtn').forEach(btn => { btn.onclick = ps_handleFilterReset; }); } function ps_handleFilterReset(event) { ps_resetFilterByKey(event.currentTarget.dataset.filterKey, true); } function ps_resetFilterByKey(key, apply = true) { switch (key) { case 'price': ps_saveFilter('priceMin', ''); if (ps_filterPriceMin) ps_filterPriceMin.value = ''; ps_saveFilter('priceMax', ''); if (ps_filterPriceMax) ps_filterPriceMax.value = ''; break; case 'sales': ps_saveFilter('salesMin', ''); if (ps_filterSalesMin) ps_filterSalesMin.value = ''; ps_saveFilter('salesMax', ''); if (ps_filterSalesMax) ps_filterSalesMax.value = ''; break; case 'rating': ps_saveFilter('ratingMin', ''); if (ps_filterRatingMin) ps_filterRatingMin.value = ''; ps_saveFilter('ratingMax', ''); if (ps_filterRatingMax) ps_filterRatingMax.value = ''; break; case 'options': ps_saveFilter('hideBadReviews', false); if (ps_filterHideBadReviews) ps_filterHideBadReviews.checked = false; ps_saveFilter('hideReturns', false); if (ps_filterHideReturns) ps_filterHideReturns.checked = false; ps_saveFilter('onlyDiscount', false); if (ps_filterOnlyDiscount) ps_filterOnlyDiscount.checked = false; break; case 'date': ps_saveFilter('date', 'all'); if (ps_filterDateSelect) ps_filterDateSelect.value = 'all'; break; } if (apply) ps_applyFilters(); } function ps_resetAllFilters(apply = true) { const filterKeys = ['price', 'sales', 'rating', 'options', 'date']; filterKeys.forEach(key => ps_resetFilterByKey(key, false)); if (apply) ps_applyFilters(); } function ps_updateFilterPlaceholders() { if (!ps_filtersPanel || !ps_currentResults || ps_currentResults.length === 0) { $('#psFilterPriceMin, #psFilterPriceMax, #psFilterSalesMin, #psFilterSalesMax, #psFilterRatingMin, #psFilterRatingMax').attr('placeholder', '-'); return; } let minPrice = Infinity, maxPrice = -Infinity, minSales = Infinity, maxSales = -Infinity, minRating = Infinity, maxRating = -Infinity; const selectedCurrency = ps_currencySelect ? ps_currencySelect.value.toUpperCase() : 'RUR'; ps_currentResults.forEach(item => { const price = getPriceInSelectedCurrency(item, selectedCurrency); const sales = formatSales(item.cnt_sell); const rating = parseSellerRating(item.seller_rating); if (price !== Infinity && price < minPrice) minPrice = price; if (price !== Infinity && price > maxPrice) maxPrice = price; if (sales < minSales) minSales = sales; if (sales > maxSales) maxSales = sales; if (rating > 0 && rating < minRating) minRating = rating; if (rating > maxRating) maxRating = rating; }); if (minRating === Infinity) minRating = 0; if (ps_filterPriceMin) ps_filterPriceMin.placeholder = minPrice === Infinity ? '-' : `от ${Math.floor(minPrice)}`; if (ps_filterPriceMax) ps_filterPriceMax.placeholder = maxPrice === -Infinity ? '-' : `до ${Math.ceil(maxPrice)}`; if (ps_filterSalesMin) ps_filterSalesMin.placeholder = minSales === Infinity ? '-' : `от ${minSales}`; if (ps_filterSalesMax) ps_filterSalesMax.placeholder = maxSales === -Infinity ? '-' : `до ${maxSales}`; if (ps_filterRatingMin) ps_filterRatingMin.placeholder = minRating === Infinity ? '-' : `от ${minRating.toFixed(1)}`; if (ps_filterRatingMax) ps_filterRatingMax.placeholder = maxRating === -Infinity ? '-' : `до ${maxRating.toFixed(1)}`; } function ps_getDateThreshold(periodKey) { const now = Date.now(); let threshold = 0; const dayMs = 86400000; switch (periodKey) { case '1d': threshold = now - 1 * dayMs; break; case '2d': threshold = now - 2 * dayMs; break; case '1w': threshold = now - 7 * dayMs; break; case '1m': threshold = now - 30 * dayMs; break; case '6m': threshold = now - 182 * dayMs; break; case '1y': threshold = now - 365 * dayMs; break; case '5y': threshold = now - 5 * 365 * dayMs; break; case '10y': threshold = now - 10 * 365 * dayMs; break; default: threshold = 0; break; } return threshold; } function ps_applyFilters() { if (!ps_resultsDiv || !ps_currentResults) return; const keywords = ps_exclusionKeywords.map(k => k.toLowerCase()); const pMin = parseFloat(ps_currentFilters.priceMin) || 0; const pMax = parseFloat(ps_currentFilters.priceMax) || Infinity; const sMin = parseInt(ps_currentFilters.salesMin, 10) || 0; const sMax = parseInt(ps_currentFilters.salesMax, 10) || Infinity; const rMin = parseFloat(ps_currentFilters.ratingMin) || 0; const rMax = parseFloat(ps_currentFilters.ratingMax) || Infinity; const hideBad = ps_currentFilters.hideBadReviews; const hideRet = ps_currentFilters.hideReturns; const onlyDisc = ps_currentFilters.onlyDiscount; const datePeriod = ps_currentFilters.date; const dateThreshold = ps_getDateThreshold(datePeriod); const selectedCurrency = ps_currencySelect ? ps_currencySelect.value.toUpperCase() : 'RUR'; let visibleCount = 0; const items = ps_resultsDiv.querySelectorAll('.platiSearchItem'); items.forEach(itemElement => { const itemId = itemElement.dataset.id; const itemData = ps_currentResults.find(r => r.id === itemId); if (!itemData) { itemElement.classList.add('hidden-by-filter'); return; } let shouldHide = false; if (!shouldHide && keywords.length > 0) { const title = (itemData.name || '').toLowerCase(); const seller = (itemData.seller_name || '').toLowerCase(); if (keywords.some(keyword => (title + ' ' + seller).includes(keyword))) { shouldHide = true; } } if (!shouldHide) { const price = getPriceInSelectedCurrency(itemData, selectedCurrency); if (price < pMin || price > pMax) { shouldHide = true; } } if (!shouldHide) { const sales = formatSales(itemData.cnt_sell); if (sales < sMin || sales > sMax) { shouldHide = true; } } if (!shouldHide) { const rating = parseSellerRating(itemData.seller_rating); if ((rating === 0 && (rMin > 0 || rMax < Infinity)) || rating < rMin || rating > rMax) { shouldHide = true; } } if (!shouldHide && hideBad) { if (parseInt(itemData.cnt_bad_responses || '0', 10) > 0) { shouldHide = true; } } if (!shouldHide && hideRet) { if (parseInt(itemData.cnt_return || '0', 10) > 0) { shouldHide = true; } } if (!shouldHide && onlyDisc) { if (parseInt(itemData.discount || '0', 10) <= 0) { shouldHide = true; } } if (!shouldHide && dateThreshold > 0) { const itemDate = parseDate(itemData.date_create); if (!itemDate || itemDate < dateThreshold) { shouldHide = true; } } if (shouldHide) { itemElement.classList.add('hidden-by-filter'); } else { itemElement.classList.remove('hidden-by-filter'); visibleCount++; } }); const totalLoadedCount = ps_currentResults.length; const anyFilterActive = pMin > 0 || pMax < Infinity || sMin > 0 || sMax < Infinity || rMin > 0 || rMax < Infinity || hideBad || hideRet || onlyDisc || datePeriod !== 'all' || keywords.length > 0; if (totalLoadedCount > 0) { if (anyFilterActive) { ps_updateStatus(`Показано ${visibleCount} из ${totalLoadedCount} товаров (фильтры/исключения применены).`); } else { ps_updateStatus(`Загружено ${totalLoadedCount} товаров.`); } } else if (ps_searchInput && ps_searchInput.value.trim()){ /* Статус уже должен быть установлен */ } else { ps_updateStatus(`Введите запрос для поиска.`); } if (visibleCount === 0 && totalLoadedCount > 0 && anyFilterActive) { ps_statusDiv.textContent += ' Нет товаров, соответствующих критериям.'; ps_statusDiv.style.display = 'block'; } else if (totalLoadedCount === 0 && ps_searchInput && ps_searchInput.value.trim()) { ps_statusDiv.style.display = 'block'; } } // --- Фильтрация Исключений --- function ps_addFilterKeyword() { const keyword = ps_excludeInput.value.trim().toLowerCase(); if (keyword && !ps_exclusionKeywords.includes(keyword)) { ps_exclusionKeywords.push(keyword); GM_setValue(PS_EXCLUSION_STORAGE_KEY, ps_exclusionKeywords); ps_excludeInput.value = ''; ps_renderExclusionTags(); ps_applyFilters(); } } function ps_removeFilterKeyword(keywordToRemove) { ps_exclusionKeywords = ps_exclusionKeywords.filter(k => k !== keywordToRemove); GM_setValue(PS_EXCLUSION_STORAGE_KEY, ps_exclusionKeywords); ps_renderExclusionTags(); ps_applyFilters(); } function ps_renderExclusionTags() { if (!ps_exclusionTagsListDiv) return; ps_exclusionTagsListDiv.innerHTML = ''; ps_exclusionKeywords.forEach(keyword => { const tag = document.createElement('span'); tag.className = 'exclusionTag'; tag.textContent = keyword; tag.title = `Удалить "${keyword}"`; tag.onclick = () => ps_removeFilterKeyword(keyword); ps_exclusionTagsListDiv.appendChild(tag); }); } // --- Рендеринг Результатов --- function ps_renderResults() { if (!ps_resultsDiv) return; ps_resultsDiv.innerHTML = ''; if (ps_currentResults.length === 0) { ps_applyFilters(); return; } const fragment = document.createDocumentFragment(); const now = Date.now(); const thresholdTime = now - NEW_ITEM_THRESHOLD_DAYS * 24 * 60 * 60 * 1000; const selectedCurrency = ps_currencySelect ? ps_currencySelect.value.toUpperCase() : 'RUR'; ps_currentResults.forEach(item => { const itemDiv = document.createElement('div'); itemDiv.className = 'platiSearchItem'; itemDiv.dataset.id = item.id; const link = document.createElement('a'); link.href = item.url || `https://plati.market/itm/${item.id}`; link.target = '_blank'; link.rel = 'noopener noreferrer nofollow'; const imageWrapper = document.createElement('div'); imageWrapper.className = 'card-image-wrapper'; const img = document.createElement('img'); const imgSrc = `https://${PS_IMAGE_DOMAIN}/imgwebp.ashx?id_d=${item.id}&w=164&h=164&dc=${item.ticks_last_change || Date.now()}`; img.src = imgSrc; img.alt = item.name || 'Изображение товара'; img.loading = 'lazy'; img.onerror = function() { this.onerror = null; this.src = 'https://plati.market/images/logo-plati.png'; this.style.objectFit = 'contain'; }; imageWrapper.appendChild(img); const itemDate = parseDate(item.date_create); if (itemDate && itemDate > thresholdTime) { const newBadge = document.createElement('span'); newBadge.className = 'newItemBadge'; newBadge.textContent = 'New'; imageWrapper.appendChild(newBadge); } link.appendChild(imageWrapper); const priceDiv = document.createElement('div'); priceDiv.className = 'price'; let displayPrice = getPriceInSelectedCurrency(item, selectedCurrency); let currencySymbol; switch (selectedCurrency) { case 'USD': currencySymbol = '$'; break; case 'EUR': currencySymbol = '€'; break; case 'UAH': currencySymbol = '₴'; break; default: currencySymbol = '₽'; break; } priceDiv.textContent = displayPrice !== Infinity ? `${displayPrice.toLocaleString('ru-RU', {minimumFractionDigits: 0, maximumFractionDigits: 2})} ${currencySymbol}` : 'Нет цены'; priceDiv.title = `Цена в ${selectedCurrency}`; link.appendChild(priceDiv); const titleDiv = document.createElement('div'); titleDiv.className = 'title'; titleDiv.textContent = item.name || 'Без названия'; titleDiv.title = item.name || 'Без названия'; link.appendChild(titleDiv); const infoContainer = document.createElement('div'); infoContainer.className = 'cardInfoContainer'; const infoRow1 = document.createElement('div'); infoRow1.className = 'cardInfoRow1'; const infoRow2 = document.createElement('div'); infoRow2.className = 'cardInfoRow2'; const ratingVal = parseSellerRating(item.seller_rating); const goodRev = parseInt(item.cnt_good_responses || '0'); const badRev = parseInt(item.cnt_bad_responses || '0'); const returns = parseInt(item.cnt_return || '0'); let salesCount = formatSales(item.cnt_sell); infoRow1.innerHTML = `<span title="Рейтинг продавца">Рейт: ${ratingVal > 0 ? ratingVal.toLocaleString('ru-RU', {maximumFractionDigits: 0}) : 'N/A'}</span><span title="Отзывы (Хорошие/Плохие)">Отз: <span class="reviewsGood">${goodRev}</span>${badRev > 0 ? '/<span class="reviewsBad">' + badRev + '</span>' : ''}</span><span title="Возвраты">Возв: ${returns}</span>`; infoRow2.innerHTML = `<span class="sales" title="Продажи">Прод: ${salesCount > 0 ? salesCount.toLocaleString('ru-RU') : '0'}</span><span class="dateAdded" title="Дата добавления">Доб: ${formatDateString(itemDate)}</span>`; infoContainer.appendChild(infoRow1); infoContainer.appendChild(infoRow2); const sellerLink = document.createElement('a'); sellerLink.className = 'sellerLink'; sellerLink.textContent = `Продавец: ${item.seller_name || 'N/A'}`; sellerLink.title = `Перейти к продавцу: ${item.seller_name || 'N/A'}`; if (item.seller_id && item.seller_name) { const safeSellerName = encodeURIComponent(item.seller_name.replace(/[^a-zA-Z0-9_\-.~]/g, '-')).replace(/%2F/g, '/'); sellerLink.href = `https://plati.market/seller/${safeSellerName}/${item.seller_id}/`; sellerLink.target = '_blank'; sellerLink.rel = 'noopener noreferrer nofollow'; sellerLink.onclick = (e) => { e.stopPropagation(); }; } else { sellerLink.style.pointerEvents = 'none'; } infoContainer.appendChild(sellerLink); link.appendChild(infoContainer); const buyButtonDiv = document.createElement('div'); buyButtonDiv.className = 'buyButton'; buyButtonDiv.textContent = 'Перейти'; link.appendChild(buyButtonDiv); itemDiv.appendChild(link); fragment.appendChild(itemDiv); }); ps_resultsDiv.appendChild(fragment); ps_applyFilters(); } // --- Обработчики UI --- function ps_handleCurrencyChange() { ps_currentCurrency = ps_currencySelect.value.toUpperCase(); GM_setValue(PS_CURRENCY_STORAGE_KEY, ps_currentCurrency); applyLoadedFiltersToUI(); ps_updateFilterPlaceholders(); if (ps_currentSort.field === 'price') { ps_applySort(ps_currentSort.field, ps_currentSort.direction); } ps_renderResults(); } // --- Добавление кнопки Plati --- function addPlatiButton() { const actionsContainer = document.querySelector('#queueActionsCtn'); const ignoreButtonContainer = actionsContainer?.querySelector('#ignoreBtn'); if (!actionsContainer || !ignoreButtonContainer || actionsContainer.querySelector('.plati_price_button')) return; const platiContainer = document.createElement('div'); platiContainer.className = 'plati_price_button queue_control_button'; platiContainer.style.marginLeft = '3px'; platiContainer.innerHTML = `<div class="btnv6_blue_hoverfade btn_medium" style="height: 32px;" title="Найти на Plati.Market"><span>Plati</span></div>`; platiContainer.querySelector('div').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showPlatiModal(); }); ignoreButtonContainer.insertAdjacentElement('afterend', platiContainer); } // --- Стили --- function addPlatiStyles() { GM_addStyle(` /* Стили Спиннера */ @keyframes platiSpin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .spinner { border: 3px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: #fff; width: 1em; height: 1em; animation: platiSpin 1s linear infinite; display: inline-block; vertical-align: middle; margin-left: 5px; } .platiSearchBtn .spinner { width: 0.8em; height: 0.8em; border-width: 2px; } /* Стили Модального окна */ #platiSearchModal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(20, 20, 25, 0.98); z-index: 9999; display: none; color: #eee; font-family: "Motiva Sans", Sans-serif, Arial; overflow-y: auto; scrollbar-color: #67c1f5 #17202d; scrollbar-width: thin; } #platiSearchModal::-webkit-scrollbar { width: 8px; } #platiSearchModal::-webkit-scrollbar-track { background: #17202d; border-radius: 4px; } #platiSearchModal::-webkit-scrollbar-thumb { background-color: #4b6f9c; border-radius: 4px; border: 2px solid #17202d; } #platiSearchModal::-webkit-scrollbar-thumb:hover { background-color: #67c1f5; } #platiSearchModal * { box-sizing: border-box; } #platiSearchContainer { max-width: 1350px; margin: 0 auto; padding: 15px ${PS_SIDE_PANEL_HORIZONTAL_PADDING}px; position: relative; min-height: 100%; } #platiSearchCloseBtn { position: fixed; top: 15px; right: 20px; font-size: 35px; color: #aaa; background: none; border: none; cursor: pointer; line-height: 1; z-index: 10002; padding: 5px; transition: color 0.2s, transform 0.2s; } #platiSearchCloseBtn:hover { color: #fff; transform: scale(1.1); } /* Шапка */ #platiSearchHeader { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; position: relative; z-index: 5; border-bottom: 1px solid #444; padding-bottom: 15px; padding-left: ${PS_CONTENT_PADDING_LEFT}px; padding-right: ${PS_CONTENT_PADDING_RIGHT}px; margin-left: -${PS_CONTENT_PADDING_LEFT}px; margin-right: -${PS_CONTENT_PADDING_RIGHT}px; flex-shrink: 0; } .platiSearchInputContainer { position: relative; flex-grow: 0.7; min-width: 200px; flex-basis: 350px; } #platiSearchInput { width: 100%; padding: 10px 15px; font-size: 16px; background-color: #333; border: 1px solid #555; color: #eee; border-radius: 4px; height: 40px; outline: none; } #platiSearchInput:focus { border-color: #67c1f5; } #platiSearchSuggestions { position: absolute; top: 100%; left: 0; right: 0; background-color: #3a3a40; border: 1px solid #555; border-top: none; border-radius: 0 0 4px 4px; max-height: 300px; overflow-y: auto; z-index: 10000; display: none; } .suggestionItem { padding: 8px 15px; cursor: pointer; color: #eee; font-size: 14px; border-bottom: 1px solid #4a4a50; } .suggestionItem:last-child { border-bottom: none; } .suggestionItem:hover { background-color: #4a4a55; } /* Кнопки в шапке */ .platiSearchBtn { padding: 10px 15px; font-size: 14px; color: white; border: none; border-radius: 4px; cursor: pointer; white-space: nowrap; height: 40px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; background-color: #555; transition: background-color 0.2s; } .platiSearchBtn:hover:not(:disabled) { background-color: #666; } .platiSearchBtn:disabled { opacity: 0.6; cursor: default; } #platiSearchGoBtn { background-color: #4D88FF; } #platiSearchGoBtn:hover { background-color: #3366CC; } .platiSearchBtn.sortBtn.active { background-color: #007bff; } .platiSearchBtn.sortBtn.active:hover { background-color: #0056b3; } #platiResetSortBtn { background-color: #777; margin-right: 5px; padding: 0 10px; } #platiResetSortBtn:hover { background-color: #888; } #platiResetSortBtn svg { width: 16px; height: 16px; fill: currentColor; } #platiResetSortBtn.active { background-color: #007bff; } #platiSearchAdvSortBtnContainer { position: relative; flex-shrink: 0; width: ${PS_ADV_SORT_CONTAINER_WIDTH}px; display: flex; justify-content: center; } #platiSearchAdvSortBtn { width: 100%; justify-content: center; overflow: hidden; text-overflow: ellipsis; } #platiSearchCurrencySelect { margin-left: 10px; background-color: #333; color: #eee; border: 1px solid #555; border-radius: 4px; height: 40px; padding: 0 8px; font-size: 14px; cursor: pointer; flex-shrink: 0; outline: none; } #platiSearchCurrencySelect:focus { border-color: #67c1f5; } /* Меню доп сортировки */ #platiSearchAdvSortMenu { display: none; position: absolute; top: 100%; left: 0; background-color: #3a3a40; border: 1px solid #555; border-radius: 4px; min-width: 100%; z-index: 10001; padding: 5px 0; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } #platiSearchAdvSortBtnContainer:hover #platiSearchAdvSortMenu { display: block; } .platiSearchSortMenuItem { display: block; padding: 8px 15px; color: #eee; font-size: 14px; cursor: pointer; white-space: nowrap; transition: background-color 0.1s; } .platiSearchSortMenuItem:hover { background-color: #4a4a55; } .platiSearchSortMenuItem.active { background-color: #007bff; color: white; } .platiSearchSortMenuItem .sortArrow { display: inline-block; margin-left: 5px; font-size: 12px; } /* Боковые панели */ #platiSearchFiltersPanel, #platiSearchExclusionTags { position: fixed; top: ${PS_TOP_OFFSET_FOR_SIDE_PANELS}px; max-height: calc(100vh - ${PS_TOP_OFFSET_FOR_SIDE_PANELS}px - ${PS_BOTTOM_OFFSET_FOR_SIDE_PANELS}px); overflow-y: auto; z-index: 1000; padding: 10px; padding-right: 15px; scrollbar-width: thin; scrollbar-color: #555 #2a2a30; background-color: transparent; transition: top 0.2s ease-in-out; } #platiSearchFiltersPanel::-webkit-scrollbar, #platiSearchExclusionTags::-webkit-scrollbar { width: 5px; } #platiSearchFiltersPanel::-webkit-scrollbar-track, #platiSearchExclusionTags::-webkit-scrollbar-track { background: rgba(42, 42, 48, 0.5); border-radius: 3px; } #platiSearchFiltersPanel::-webkit-scrollbar-thumb, #platiSearchExclusionTags::-webkit-scrollbar-thumb { background-color: rgba(85, 85, 85, 0.7); border-radius: 3px; } #platiSearchFiltersPanel { left: ${PS_SIDE_PANEL_HORIZONTAL_PADDING}px; width: ${PS_FILTER_PANEL_WIDTH}px; } #platiSearchExclusionTags { right: ${PS_SIDE_PANEL_HORIZONTAL_PADDING}px; width: ${PS_EXCLUSION_PANEL_WIDTH}px; display: flex; flex-direction: column; gap: 10px; } /* Фильтры */ .filterGroup { margin-bottom: 18px; } .filterGroup h4 { font-size: 15px; color: #ddd; margin-bottom: 8px; padding-bottom: 4px; display: flex; justify-content: space-between; align-items: center; text-shadow: 1px 1px 2px rgba(0,0,0,0.7); font-weight: 500; } .filterResetBtn { font-size: 12px; color: #aaa; background: none; border: none; cursor: pointer; padding: 0 3px; line-height: 1; } .filterResetBtn:hover { color: #fff; } .filterResetBtn svg { width: 14px; height: 14px; vertical-align: middle; fill: currentColor; } .filterRangeInputs { display: flex; gap: 8px; align-items: center; } .filterRangeInputs input[type="number"] { width: calc(50% - 4px); padding: 6px 8px; font-size: 13px; background-color: rgba(51,51,51,0.85); border: 1px solid #666; color: #eee; border-radius: 3px; height: 30px; text-align: center; -moz-appearance: textfield; box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); outline: none; } .filterRangeInputs input[type="number"]:focus { border-color: #67c1f5; } .filterRangeInputs input[type="number"]::-webkit-outer-spin-button, .filterRangeInputs input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .filterRangeInputs input[type="number"]::placeholder { color: #999; font-size: 11px; text-align: center; } .filterCheckbox { margin-bottom: 8px; } .filterCheckbox label { display: flex; align-items: center; font-size: 14px; cursor: pointer; color: #ccc; text-shadow: 1px 1px 2px rgba(0,0,0,0.7); } .filterCheckbox input[type="checkbox"] { margin-right: 8px; width: 16px; height: 16px; accent-color: #007bff; cursor: pointer; flex-shrink: 0; } .filterSelect select { width: 100%; padding: 6px 8px; font-size: 13px; background-color: rgba(51,51,51,0.85); border: 1px solid #666; color: #eee; border-radius: 3px; height: 30px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); outline: none; } .filterSelect select:focus { border-color: #67c1f5; } #psResetAllFiltersBtn { width: 100%; margin-top: 10px; padding: 8px 10px; height: auto; background-color: rgba(108, 117, 125, 0.8); border: 1px solid #888; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); } #psResetAllFiltersBtn:hover { background-color: rgba(90, 98, 104, 0.9); } /* Исключения */ .exclusionInputGroup { display: flex; align-items: stretch; border: 1px solid #555; border-radius: 4px; background-color: rgba(51,51,51,0.85); overflow: hidden; height: 34px; flex-shrink: 0; box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); } .exclusionInputGroup #platiSearchExcludeInput { padding: 6px 10px; font-size: 13px; background-color: transparent; border: none; color: #eee; outline: none; border-radius: 0; flex-grow: 1; width: auto; height: auto; } .exclusionInputGroup #platiSearchExcludeInput:focus { box-shadow: none; } .exclusionInputGroup #platiSearchAddExcludeBtn { display: flex; align-items: center; justify-content: center; padding: 0 10px; background-color: #555; border: none; border-left: 1px solid #555; cursor: pointer; border-radius: 0; color: #eee; height: auto; } .exclusionInputGroup #platiSearchAddExcludeBtn:hover { background-color: #666; } .exclusionInputGroup #platiSearchAddExcludeBtn svg { width: 16px; height: 16px; fill: currentColor; } #platiExclusionTagsList { display: flex; flex-direction: row; flex-wrap: wrap; align-content: flex-start; gap: 8px; overflow-y: auto; flex-grow: 1; } .exclusionTag { display: inline-block; background-color: rgba(70,70,80,0.9); color: #ddd; padding: 5px 10px; border-radius: 15px; font-size: 13px; cursor: pointer; transition: background-color 0.2s; border: 1px solid rgba(100,100,110,0.9); white-space: nowrap; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } .exclusionTag:hover { background-color: rgba(220,53,69,0.9); border-color: rgba(200,40,50,0.95); color: #fff; } .exclusionTag::after { content: ' ×'; font-weight: bold; margin-left: 4px; } /* Результаты */ #platiSearchResultsContainer { position: relative; padding-left: ${PS_CONTENT_PADDING_LEFT}px; padding-right: ${PS_CONTENT_PADDING_RIGHT}px; margin-left: -${PS_CONTENT_PADDING_LEFT}px; margin-right: -${PS_CONTENT_PADDING_RIGHT}px; } #platiSearchResultsStatus { width: 100%; text-align: center; font-size: 18px; color: #aaa; padding: 50px 0; display: none; min-height: 100px; display: flex; align-items: center; justify-content: center; flex-direction: column;} #platiSearchResults { display: flex; flex-wrap: wrap; gap: 15px; justify-content: flex-start; padding-top: 10px; } /* Карточка товара */ .platiSearchItem { background-color: #2a2a30; border-radius: 8px; padding: 10px; width: calc(20% - 12px); min-width: 170px; display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.2); position: relative; color: #ccc; font-size: 13px; min-height: 340px; border: 1px solid transparent; } .platiSearchItem:hover { transform: translateY(-3px); box-shadow: 0 4px 10px rgba(0,0,0,0.4); border-color: #4b6f9c; } .platiSearchItem.hidden-by-filter { display: none !important; } .platiSearchItem a { text-decoration: none; color: inherit; display: flex; flex-direction: column; height: 100%; } .platiSearchItem .card-image-wrapper { position: relative; width: 100%; aspect-ratio: 1 / 1; margin-bottom: 8px; background-color: #444; border-radius: 6px; overflow: hidden; } .platiSearchItem img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 6px; } .newItemBadge { position: absolute; top: 4px; left: 4px; background-color: #f54848; color: white; padding: 1px 5px; font-size: 10px; border-radius: 3px; font-weight: bold; z-index: 1; text-shadow: 1px 1px 1px rgba(0,0,0,0.3); } .platiSearchItem .price { font-size: 16px; font-weight: 700; color: #a4d007; margin-bottom: 5px; } .platiSearchItem .title { font-size: 13px; font-weight: 500; line-height: 1.3; height: 3.9em; overflow: hidden; text-overflow: ellipsis; margin-bottom: 6px; color: #eee; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; } .cardInfoContainer { margin-top: auto; padding-top: 6px; } .cardInfoRow1, .cardInfoRow2 { display: flex; justify-content: space-between; flex-wrap: nowrap; gap: 8px; font-size: 12px; color: #bbb; margin-bottom: 4px; } .cardInfoRow1 span, .cardInfoRow2 span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; } .cardInfoRow1 span:first-child, .cardInfoRow2 span:first-child { flex-shrink: 0; margin-right: auto; } .reviewsGood { color: #6cff5c; font-weight: bold; } .reviewsBad { color: #f54848; margin-left: 2px; font-weight: bold;} .sales { font-weight: bold; color: #eee; } .sellerLink { display: block; font-size: 12px; color: #bbb; text-decoration: none; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.2s; } .sellerLink:hover { color: #ddd; text-decoration: underline; } .platiSearchItem .buyButton { display: block; text-align: center; padding: 8px; margin-top: 8px; background-color: #007bff; color: white; border-radius: 4px; font-size: 13px; font-weight: 600; transition: background-color 0.2s; } .platiSearchItem .buyButton:hover { background-color: #0056b3; } /* Адаптивность */ @media (max-width: 1650px) { .platiSearchItem { width: calc(20% - 12px); } } @media (max-width: 1400px) { .platiSearchItem { width: calc(25% - 12px); } } @media (max-width: 1100px) { .platiSearchItem { width: calc(33.33% - 10px); } } @media (max-width: 850px) { #platiSearchFiltersPanel, #platiSearchExclusionTags { display: none; } #platiSearchHeader, #platiSearchResultsContainer { padding-left: 15px; padding-right: 15px; margin-left: 0; margin-right: 0; } .platiSearchItem { width: calc(50% - 8px); } #platiSearchHeader { justify-content: center; } } @media (max-width: 600px) { .platiSearchItem { width: 100%; min-height: auto; } #platiSearchHeader { gap: 5px; } .platiSearchInputContainer { flex-basis: 100%; order: -1; } .platiSearchBtn, #platiSearchCurrencySelect, #platiSearchAdvSortBtnContainer { width: calc(33.3% - 4px); font-size: 13px; padding: 8px 5px; height: 36px; } #platiSearchAdvSortBtnContainer { width: calc(33.3% - 4px); } #platiSearchAdvSortBtn { width: 100%; } #platiSearchAdvSortMenu { min-width: 200px; left: 50%; transform: translateX(-50%); } #platiResetSortBtn { width: auto; padding: 0 8px; } } /* Стили для кнопки Plati на странице Steam */ .plati_price_button .btnv6_blue_hoverfade { margin: 0; padding: 0 15px; font-size: 15px; display: flex; align-items: center; transition: filter 0.2s; } .plati_price_button .btnv6_blue_hoverfade:hover { filter: brightness(1.1); } `); } // --- Инициализация модуля --- addPlatiStyles(); const steamAppIdCheck = window.location.pathname.match(/\/app\/(\d+)/); if (steamAppIdCheck && steamAppIdCheck[1]) { addPlatiButton(); } })(); } // Скрипт для страницы игры (VGT; отображения цен из агрегатора VGTimes) | https://store.steampowered.com/app/* if (scriptsConfig.vgtSales && window.location.pathname.includes('/app/')) { (function() { 'use strict'; const VGT_DATA_URL = 'https://gist.githubusercontent.com/0wn3dg0d/2644d328cca76b74c57804c7303b8606/raw/vgtstulex.json'; const VGT_API_URL = 'https://vgtimes.ru/engine/modules/games/shops_table.php'; const ITEMS_PER_PAGE = 40; let vgtDataMap = new Map(); let vgtSteamMap = new Map(); let vgtNameMap = new Map(); function addVGTButton() { const actionsContainer = document.querySelector('#queueActionsCtn'); // Находим элемент кнопки "Скрыть" (Ignore) по его ID const ignoreButtonContainer = actionsContainer?.querySelector('#ignoreBtn'); // Проверяем, что оба элемента найдены if (!actionsContainer || !ignoreButtonContainer) { console.warn('VGT Button: Could not find actions container or ignore button container.'); return; // Выходим, если не нашли нужные элементы } const vgtContainer = document.createElement('div'); // Добавляем класс для стилизации и идентификации vgtContainer.className = 'vgt_price_button queue_control_button'; // Добавляем queue_control_button для выравнивания // Убираем правый отступ и добавляем левый отступ в 3px vgtContainer.style.marginLeft = '3px'; // vgtContainer.style.marginRight = '4px'; // Убираем этот стиль vgtContainer.innerHTML = ` <div class="btnv6_blue_hoverfade btn_medium" style="height: 32px;"> <span>Цены (VGT)</span> </div> `; vgtContainer.querySelector('div').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openVGTModal(); }); // Вставляем кнопку VGT *после* контейнера кнопки "Скрыть" ignoreButtonContainer.insertAdjacentElement('afterend', vgtContainer); } async function openVGTModal() { const vgtModal = createModal(); document.body.appendChild(vgtModal); showLoading(vgtModal); setupModalEvents(vgtModal); try { await loadVGTData(); const appId = getAppId(); const gameData = await findGameData(appId); if (!gameData) { showError(vgtModal, 'Игра не найдена в базе VGTimes'); return; } initModalContent(vgtModal, gameData); await loadPrices(gameData['data-id'], vgtModal, 0); } catch (error) { showError(vgtModal, 'Ошибка загрузки данных'); console.error('VGT Error:', error); } } async function loadVGTData() { const data = await fetchJson(VGT_DATA_URL); vgtDataMap = new Map(Object.entries(data)); vgtSteamMap = new Map(); vgtNameMap = new Map(); for (const [key, value] of Object.entries(data)) { if (value.steam !== null) { vgtSteamMap.set(String(value.steam), value); } const normalized = normalizeName(value.title); vgtNameMap.set(normalized, value); } } async function findGameData(appId) { if (vgtSteamMap.has(appId)) { return vgtSteamMap.get(appId); } const gameName = getGameName(); const normalized = normalizeName(gameName); if (vgtNameMap.has(normalized)) return vgtNameMap.get(normalized); const possibleMatches = findPossibleMatches(gameName, Array.from(vgtDataMap.values())); if (possibleMatches.length === 0) return null; const selectedGame = await showGameSelection(possibleMatches); return selectedGame; } function normalizeName(name) { return name .normalize("NFD").replace(/[\u0300-\u036f]/g, "") .replace(/[’]/g, "'") .replace(/[^a-zA-Zа-яёА-ЯЁ0-9 _'\-!]/g, '') .trim() .toLowerCase(); } function findPossibleMatches(gameName, games) { const cleanGameName = normalizeName(gameName); return games .map(game => { const cleanTitle = normalizeName(game.title); const similarity = calculateSimilarity(cleanGameName, cleanTitle); return { ...game, similarity }; }) .filter(game => game.similarity > 50) .sort((a, b) => b.similarity - a.similarity) .slice(0, 5); } function calculateSimilarity(a, b) { const maxLen = Math.max(a.length, b.length); if (maxLen === 0) return 0; const distance = levenshteinDistance(a, b); return Math.round((1 - distance / maxLen) * 100); } function levenshteinDistance(a, b) { const matrix = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0)); for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; matrix[i][j] = Math.min( matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost ); } } return matrix[a.length][b.length]; } async function showGameSelection(games) { return new Promise(resolve => { const modal = document.querySelector('.vgt_modal'); const content = modal.querySelector('.vgt_content'); content.innerHTML = ` <div class="vgt_selection"> <h3>Выберите игру:</h3> <div class="vgt_games_list"> ${games.map(game => ` <div class="vgt_game_item" data-id="${game['data-id']}"> <div class="vgt_game_title">${game.title}</div> <div class="vgt_game_similar">Совпадение: ${game.similarity}%</div> </div> `).join('')} </div> <div class="vgt_selection_buttons"> <button class="vgt_cancel_btn">Ничего не подходит</button> </div> </div> `; modal.querySelectorAll('.vgt_game_item').forEach(item => { item.addEventListener('click', () => { const selectedId = item.dataset.id; const selectedGame = vgtDataMap.get(selectedId); content.innerHTML = ''; resolve(selectedGame); }); }); modal.querySelector('.vgt_cancel_btn').addEventListener('click', () => { content.innerHTML = ''; resolve(null); }); }); } function createModal() { const modal = document.createElement('div'); modal.className = 'vgt_modal'; modal.innerHTML = ` <div class="vgt_modal-overlay"></div> <div class="vgt_modal-content"> <span class="vgt_close">×</span> <div class="vgt_header"></div> <div class="vgt_content"></div> </div> `; return modal; } function initModalContent(modal, gameData) { const header = modal.querySelector('.vgt_header'); header.innerHTML = `<h2><a href="${gameData.url}" target="_blank">${gameData.title}</a></h2>`; } function setupModalEvents(modal) { modal.querySelector('.vgt_modal-overlay').addEventListener('click', () => modal.remove()); const closeBtn = modal.querySelector('.vgt_close'); closeBtn.addEventListener('mouseenter', () => closeBtn.style.color = '#67c1f5'); closeBtn.addEventListener('mouseleave', () => closeBtn.style.color = '#aaa'); closeBtn.onclick = () => modal.remove(); } async function loadPrices(dataId, modal, skip) { const content = modal.querySelector('.vgt_content'); content.innerHTML = '<div class="vgt_loading">Загрузка цен...</div>'; try { const params = new URLSearchParams({ skin: 'vgtimes', id: dataId, skip: skip, sort: 'rele', shop: 'all', payment_method: 'all', platform: 'all', custom_filter: '' }); const response = await postRequest(VGT_API_URL, params.toString()); const data = JSON.parse(response.responseText); if (data.offercount === 0 || data.result.includes('notf gp_lb')) { content.innerHTML = '<div class="vgt_error">Информация о ценах отсутствует в базе VGTimes.</div>'; return; } const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.result; if (tempDiv.querySelector('.notf.gp_lb')) { content.innerHTML = '<div class="vgt_error">Информация о ценах отсутствует в базе VGTimes.</div>'; return; } tempDiv.querySelectorAll('.int, .s_rating, .s_reviews, .boosted, .s_promocodes, .promocode').forEach(el => el.remove()); processElements(tempDiv); const shops = groupByShops(tempDiv); const sortedShops = sortShops(shops); content.innerHTML = generateShopColumns(sortedShops); addExpandHandlers(content); if (data.offercount > skip + ITEMS_PER_PAGE) { const loadMoreBtn = document.createElement('button'); loadMoreBtn.className = 'vgt_load_more'; loadMoreBtn.textContent = 'Загрузить ещё'; loadMoreBtn.onclick = () => loadPrices(dataId, modal, skip + ITEMS_PER_PAGE); content.appendChild(loadMoreBtn); } } catch (error) { content.innerHTML = '<div class="vgt_error">Ошибка загрузки цен</div>'; } } function processElements(container) { container.querySelectorAll('img').forEach(img => img.remove()); container.querySelectorAll('a').forEach(link => { let href = link.getAttribute('href'); try { if (href.startsWith('/')) { href = 'https://vgtimes.ru' + href; } if (href.includes('/shop_redirect')) { const urlObj = new URL(href); const realUrl = urlObj.searchParams.get('url'); if (realUrl) { href = decodeURIComponent(realUrl) .replace(/(https?:\/\/)?store\.steampowered\.com\/?/i, '') .replace(/^\/+/g, ''); const cleanUrl = href.split('?')[0]; href = cleanUrl.startsWith('http') ? cleanUrl : `https://${cleanUrl}`; } } if (link.classList.contains('shopm')) { const shopPath = new URL(href).pathname; href = `https://vgtimes.ru${shopPath}`; } link.href = href; } catch (e) { console.error('URL processing error:', e); link.href = '#'; } link.removeAttribute('style'); }); } function groupByShops(container) { const shopsMap = new Map(); container.querySelectorAll('.products_search_par').forEach(item => { const shopElement = item.querySelector('.shopm'); if (!shopElement) return; const shopName = shopElement.textContent.trim(); if (!shopsMap.has(shopName)) { shopsMap.set(shopName, { name: shopName, items: [], minPrice: Infinity, url: shopElement.href }); } const priceElement = item.querySelector('.aprice'); let price = parsePrice(priceElement.textContent); const shopData = shopsMap.get(shopName); shopData.items.push(item); if (price < shopData.minPrice) shopData.minPrice = price; }); return Array.from(shopsMap.values()); } function parsePrice(priceText) { if (priceText.toLowerCase() === 'бесплатно') return 0; const number = priceText.replace(/[^0-9,]/g, '').replace(',', '.'); return parseFloat(number) || Infinity; } function sortShops(shops) { return shops.sort((a, b) => a.minPrice - b.minPrice); } function generateShopColumns(shops) { return ` <div class="vgt_columns"> ${shops.map(shop => ` <div class="vgt_shop_column"> <div class="vgt_shop_header"> <a href="${shop.url}" target="_blank">${shop.name}</a> </div> <div class="vgt_shop_items"> ${shop.items.slice(0, 3).map(item => generateItemHtml(item)).join('')} </div> ${shop.items.length > 3 ? ` <div class="vgt_shop_expand" data-shop="${shop.name}" data-expanded="false"> <div class="vgt_expand_content" style="display: none;"> ${shop.items.slice(3).map(item => generateItemHtml(item)).join('')} </div> <div class="vgt_expand_toggle">...</div> </div> ` : ''} </div> `).join('')} </div> `; } function generateItemHtml(item) { const title = item.querySelector('.title').textContent.trim(); const priceElement = item.querySelector('.aprice'); const oldPriceElement = item.querySelector('.oldprice'); const discountElement = item.querySelector('.percent'); const link = item.querySelector('a.f_click').href; return ` <div class="vgt_shop_item"> <a href="${link}" target="_blank" class="vgt_item_link"> <div class="vgt_item_title">${title}</div> <div class="vgt_prices"> ${oldPriceElement ? `<span class="vgt_oldprice">${oldPriceElement.textContent}</span>` : ''} <span class="vgt_aprice">${priceElement.textContent}</span> ${discountElement ? `<span class="vgt_percent">${discountElement.textContent}</span>` : ''} </div> </a> </div> `; } function addExpandHandlers(content) { content.querySelectorAll('.vgt_shop_expand').forEach(expand => { expand.querySelector('.vgt_expand_toggle').addEventListener('click', () => { const isExpanded = expand.dataset.expanded === 'true'; expand.dataset.expanded = !isExpanded; expand.querySelector('.vgt_expand_content').style.display = isExpanded ? 'none' : 'block'; expand.querySelector('.vgt_expand_toggle').textContent = isExpanded ? '...' : '▲'; }); }); } function getAppId() { return window.location.pathname.split('/')[2]; } function getGameName() { return document.querySelector('.apphub_AppName')?.textContent || ''; } function fetchJson(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: (r) => resolve(JSON.parse(r.responseText)), onerror: reject }); }); } function postRequest(url, body) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, data: body, onload: resolve, onerror: reject }); }); } function showLoading(modal) { modal.querySelector('.vgt_content').innerHTML = '<div class="vgt_loading">Загрузка...</div>'; } function showError(modal, message) { modal.querySelector('.vgt_content').innerHTML = `<div class="vgt_error">${message}</div>`; } GM_addStyle(` .vgt_modal { display: block; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; } .vgt_modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); } .vgt_modal-content { background: #1b2838; margin: 5% auto; padding: 20px; border: 1px solid #67c1f5; width: 90%; max-width: 1200px; color: #c6d4df; position: relative; max-height: 80vh; overflow-y: auto; z-index: 10001; } .vgt_columns { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-top: 20px; } .vgt_shop_column { background: #16202d; border-radius: 4px; padding: 15px; border: 1px solid #2a475e; } .vgt_shop_header { font-weight: bold; font-size: 16px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #67c1f5; } .vgt_shop_header a { color: #67c1f5 !important; text-decoration: none; } .vgt_shop_item { margin: 10px 0; padding: 10px; background: rgba(27, 40, 56, 0.7); border-radius: 3px; } .vgt_item_title { font-size: 14px; margin-bottom: 5px; line-height: 1.3; } .vgt_prices { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .vgt_aprice { color: #5ba32b; font-weight: bold; font-size: 15px; } .vgt_oldprice { text-decoration: line-through; opacity: 0.6; font-size: 13px; } .vgt_percent { color: #67c1f5; font-size: 13px; } .vgt_shop_expand { text-align: center; margin-top: 10px; cursor: pointer; } .vgt_expand_toggle { color: #67c1f5; font-weight: bold; padding: 5px; transition: opacity 0.2s; } .vgt_expand_toggle:hover { opacity: 0.8; } .vgt_load_more { background: #67c1f5; color: #1b2838; border: none; padding: 10px 20px; margin: 20px auto 0; display: block; cursor: pointer; border-radius: 3px; transition: background 0.2s; } .vgt_load_more:hover { background: #4fa0d1; } .vgt_close { color: #aaa; position: absolute; right: 20px; top: 10px; font-size: 28px; cursor: pointer; transition: color 0.2s; } .vgt_close:hover { color: #67c1f5; } .vgt_loading, .vgt_error { text-align: center; padding: 20px; font-size: 16px; } .vgt_price_button .btnv6_blue_hoverfade { /* Убери 'height: 32px;' отсюда, если добавил его инлайн в HTML */ margin: 0; padding: 0 15px; font-size: 15px; /* height: 32px; */ /* Можно оставить тут или задать инлайн */ display: flex; /* Для выравнивания текста внутри */ align-items: center; /* Для выравнивания текста внутри */ } .vgt_selection { padding: 20px; text-align: center; } .vgt_games_list { display: grid; gap: 10px; margin-top: 15px; } .vgt_game_item { padding: 15px; background: #16202d; border-radius: 4px; cursor: pointer; transition: background 0.2s; } .vgt_game_item:hover { background: #1b2838; } .vgt_game_title { color: #67c1f5; font-weight: bold; margin-bottom: 5px; } .vgt_game_similar { color: #8f98a0; font-size: 12px; } .vgt_selection_buttons { margin-top: 20px; text-align: center; } .vgt_cancel_btn { background: #a34d4d; color: #fff; border: none; padding: 8px 20px; cursor: pointer; border-radius: 3px; transition: background 0.2s; } .vgt_cancel_btn:hover { background: #c25555; } `); addVGTButton(); })(); } })();