// ==UserScript== // @name SteamDB - Sales; Ultimate Enhancer // @namespace https://steamdb.info/ // @version 1.0 // @description Комплексное улучшение для SteamDB: фильтры по языкам, спискам и дате, конвертация валют, расширенная информация об играх // @author 0wn3df1x // @license MIT // @include https://steamdb.info/sales/* // @grant GM_xmlhttpRequest // @connect api.steampowered.com // @downloadURL none // ==/UserScript== (function() { 'use strict'; const scriptsConfig = { toggleEnglishLangInfo: false }; const API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1"; const BATCH_SIZE = 100; const HOVER_DELAY = 300; const REQUEST_DELAY = 200; const DEFAULT_EXCHANGE_RATE = 0.19; let collectedAppIds = new Set(); let tooltip = null; let hoverTimer = null; let gameData = {}; let activeLanguageFilter = null; let totalGames = 0; let processedGames = 0; let progressContainer = null; let requestQueue = []; let isProcessingQueue = false; let currentExchangeRate = DEFAULT_EXCHANGE_RATE; let activeListFilter = false; let activeDateFilterTimestamp = null; let isProcessingStarted = false; let processButton = null; const PROCESS_BUTTON_TEXT = { idle: "Обработать игры", processing: "Обработка...", done: "Обработка завершена" }; const styles = ` .steamdb-enhancer * { box-sizing: border-box; margin: 0; padding: 0; } .steamdb-enhancer { background: #16202d; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); padding: 12px; width: 50%; margin-top: 5px; margin-bottom: 15px; } .enhancer-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; } .row-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } .row-layout.compact { gap: 8px; margin-bottom: 0; } .control-group { background: #1a2635; border-radius: 6px; padding: 10px; margin: 6px 0; } .group-title { color: #66c0f4; font-size: 12px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; } .btn-group { display: flex; flex-wrap: wrap; gap: 5px; } .btn { background: #2a3a4d; border: 1px solid #354658; border-radius: 4px; color: #c6d4df; cursor: pointer; font-size: 12px; padding: 5px 10px; transition: all 0.2s ease; display: flex; align-items: center; gap: 5px; white-space: nowrap; } .btn:hover { background: #31455b; border-color: #3d526b; } .btn.active { background: #66c0f4 !important; border-color: #66c0f4 !important; color: #1b2838 !important; } .btn-icon { width: 12px; height: 12px; fill: currentColor; } .progress-container { background: #1a2635; border-radius: 4px; height: 6px; overflow: hidden; margin: 10px 0 5px; } .progress-text { display: flex; justify-content: space-between; color: #8f98a0; font-size: 11px; margin: 4px 2px 0; } .progress-count { flex: 1; text-align: left; } .progress-percent { flex: 1; text-align: right; } .progress-bar { height: 100%; background: linear-gradient(90deg, #66c0f4 0%, #4d9cff 100%); transition: width 0.3s ease; } .steamdb-tooltip { background: #1a2635; border: 1px solid #2a3a4d; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); padding: 12px; max-width: 320px; font-size: 13px; line-height: 1.5; position: absolute; z-index: 10000; opacity: 0; transition: opacity 0.2s; pointer-events: none; } .converter-group { display: flex; gap: 6px; flex: 1; } .input-field { background: #1a2635; border: 1px solid #2a3a4d; border-radius: 4px; color: #c6d4df; font-size: 12px; padding: 5px 8px; min-width: 60px; } .date-picker { background: #1a2635; border: 1px solid #2a3a4d; border-radius: 4px; color: #c6d4df; font-size: 12px; padding: 5px; width: 120px; } .status-indicator { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 5px 8px; border-radius: 4px; } .steamdb-tooltip { position: absolute; background: #1b2838; color: #c6d4df; padding: 15px; border-radius: 3px; width: 320px; font-size: 14px; line-height: 1.5; box-shadow: 0 0 12px rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s; pointer-events: none; z-index: 9999; } .tooltip-arrow { position: absolute; left: -10px; top: 20px; width: 0; height: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 10px solid #1b2838; } .group-top { margin-bottom: 8px; } .group-middle { margin-bottom: 12px; } .group-bottom { margin-bottom: 15px; } .tooltip-row.compact { margin-bottom: 2px; } .tooltip-row.spaced { margin-bottom: 10px; } .tooltip-row.language { margin-bottom: 8px; } .tooltip-row.description { margin-top: 15px; padding-top: 10px; border-top: 1px solid #2a3a4d; color: #8f98a0; font-style: italic; } .positive { color: #66c0f4; } .mixed { color: #997a00; } .negative { color: #a74343; } .no-reviews { color: #929396; } .language-yes { color: #66c0f4; } .language-no { color: #a74343; } .early-access-yes { color: #66c0f4; } .early-access-no { color: #929396; } .no-data { color: #929396; } `; function createFiltersContainer() { const container = document.createElement('div'); container.className = 'steamdb-enhancer'; container.innerHTML = `
Выберите All (slow) entries per page и нажмите на кнопку "Обработать игры".
0/0 (0%)
Русский перевод
Списки
Дополнительные инструменты
`; return container; } function handleFilterClick(event) { const btn = event.target.closest('[data-filter]'); if (!btn) return; const filterType = btn.dataset.filter; const wasActive = btn.classList.contains('active'); document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active')); if (!wasActive) { btn.classList.add('active'); activeLanguageFilter = filterType; } else { activeLanguageFilter = null; } applyAllFilters(); } function handleControlClick(event) { const btn = event.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; switch (action) { case 'list1': saveList('list1'); break; case 'list2': saveList('list2'); break; case 'list-filter': activeListFilter = !activeListFilter; btn.classList.toggle('active', activeListFilter); applyAllFilters(); break; case 'convert': currentExchangeRate = parseFloat(document.querySelector('.input-field').value) || DEFAULT_EXCHANGE_RATE; convertPrices(); break; case 'date-filter': { const dateInput = btn.previousElementSibling; if (btn.classList.contains('active')) { btn.classList.remove('active'); activeDateFilterTimestamp = null; } else { activeDateFilterTimestamp = new Date(dateInput.value).getTime() / 1000; btn.classList.add('active'); } applyAllFilters(); break; } } } function saveList(listName) { const appIds = Array.from(collectedAppIds); localStorage.setItem(listName, JSON.stringify(appIds)); alert(`Список ${listName} сохранён (${appIds.length} игр)`); } function convertPrices() { document.querySelectorAll('tr.app').forEach(row => { const priceElements = row.querySelectorAll('td.dt-type-numeric'); if (priceElements.length < 3) return; const priceElement = priceElements[2]; const priceText = priceElement.textContent.trim(); let priceValue; if (priceText.includes('S/.')) { const priceMatch = priceText.match(/S\/\.([0-9,.]+)/); priceValue = priceMatch ? parseFloat(priceMatch[1].replace(',', '.')) : 0; } else { const priceMatch = priceText.match(/([0-9,.]+)/); priceValue = priceMatch ? parseFloat(priceMatch[1].replace(',', '.')) : 0; } if (!isNaN(priceValue)) { const converted = (priceValue * currentExchangeRate).toFixed(2); priceElement.textContent = `${converted}`; } }); } function applyAllFilters() { const rows = document.querySelectorAll('tr.app'); const list1 = JSON.parse(localStorage.getItem('list1') || '[]'); const list2 = JSON.parse(localStorage.getItem('list2') || '[]'); const commonIds = new Set(list1.filter(id => list2.includes(id))); rows.forEach(row => { const appId = row.dataset.appid; const data = gameData[appId]; let visible = true; if (activeListFilter) visible = !commonIds.has(appId); if (visible && activeDateFilterTimestamp !== null) { const cells = row.querySelectorAll('.timeago'); const startTime = parseInt(cells[1]?.dataset.sort || cells[0]?.dataset.sort || '0'); visible = startTime >= activeDateFilterTimestamp; } if (visible && activeLanguageFilter) { const lang = data?.language_support_russian || {}; switch (activeLanguageFilter) { case 'russian-any': visible = (lang.supported || lang.subtitles) && !lang.full_audio; break; case 'russian-audio': visible = lang.full_audio; break; case 'no-russian': visible = !lang.supported && !lang.full_audio && !lang.subtitles; break; } } row.style.display = visible ? '' : 'none'; }); } function processGameData(items) { items.forEach(item => { if (!item?.id) return; gameData[item.id] = { franchises: item.basic_info?.franchises?.map(f => f.name).join(', '), percent_positive: item.reviews?.summary_filtered?.percent_positive, review_count: item.reviews?.summary_filtered?.review_count, is_early_access: item.is_early_access, short_description: item.basic_info?.short_description, language_support_russian: item.supported_languages?.find(l => l.elanguage === 8), language_support_english: item.supported_languages?.find(l => l.elanguage === 0) }; processedGames++; updateProgress(); }); } async function processRequestQueue() { if (isProcessingQueue || !requestQueue.length) return; isProcessingQueue = true; while (requestQueue.length) { const batch = requestQueue.shift(); try { await fetchGameData(batch); await new Promise(r => setTimeout(r, REQUEST_DELAY)); } catch (error) { console.error('Batch error:', error); } } isProcessingQueue = false; } function fetchGameData(appIds) { return new Promise((resolve, reject) => { const input = { ids: Array.from(appIds).map(appid => ({ appid })), context: { language: "russian", country_code: "US", steam_realm: 1 }, data_request: { include_assets: true, include_release: true, include_platforms: true, include_all_purchase_options: true, include_screenshots: true, include_trailers: true, include_ratings: true, include_tag_count: true, include_reviews: true, include_basic_info: true, include_supported_languages: true, include_full_description: true, include_included_items: true } }; GM_xmlhttpRequest({ method: "GET", url: `${API_URL}?input_json=${encodeURIComponent(JSON.stringify(input))}`, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); processGameData(data.response.store_items); resolve(); } catch (e) { console.error('Error parsing JSON:', e); processedGames += appIds.length; updateProgress(); resolve(); } } else { console.error('API request failed:', response.statusText); processedGames += appIds.length; updateProgress(); resolve(); } }, onerror: function(error) { console.error('API request error:', error); processedGames += appIds.length; updateProgress(); resolve(); } }); }); } function collectAppIds() { const rows = document.querySelectorAll('tr.app[data-appid]'); totalGames = rows.length; const newIds = new Set( Array.from(rows) .map(r => r.dataset.appid) .filter(id => !collectedAppIds.has(id)) ); if (newIds.size) { collectedAppIds = new Set([...collectedAppIds, ...newIds]); const batches = []; const arr = Array.from(newIds); while (arr.length) batches.push(arr.splice(0, BATCH_SIZE)); requestQueue.push(...batches); processRequestQueue(); } updateProgress(); } function updateProgress() { const progressBar = document.querySelector('.progress-bar'); const progressCount = document.querySelector('.progress-count'); const progressPercent = document.querySelector('.progress-percent'); if (!progressBar || !progressCount || !progressPercent) return; const percent = (processedGames / totalGames) * 100; progressBar.style.width = `${percent}%`; progressCount.textContent = `${processedGames}/${totalGames}`; progressPercent.textContent = `(${Math.round(percent)}%)`; if (processedGames === totalGames) { document.querySelector('#process-btn').textContent = PROCESS_BUTTON_TEXT.done; document.querySelector('.status-indicator').classList.add('status-active'); } } function handleHover(event) { const row = event.target.closest('tr.app'); if (!row) return; clearTimeout(hoverTimer); hoverTimer = setTimeout(() => { const appId = row.dataset.appid; if (gameData[appId]) showTooltip(row, gameData[appId]); }, HOVER_DELAY); row.addEventListener('mouseleave', () => { clearTimeout(hoverTimer); if (tooltip) tooltip.style.opacity = '0'; }, { once: true }); } function showTooltip(element, data) { if (!tooltip) { tooltip = document.createElement('div'); tooltip.className = 'steamdb-tooltip'; tooltip.innerHTML = `
${buildTooltipContent(data)}
`; document.body.appendChild(tooltip); } else { tooltip.querySelector('.tooltip-content').innerHTML = buildTooltipContent(data); } const rect = element.getBoundingClientRect(); tooltip.style.left = `${rect.right + window.scrollX}px`; tooltip.style.top = `${rect.top + window.scrollY - 8}px`; tooltip.style.opacity = '1'; } function buildTooltipContent(data) { const reviewClass = getReviewClass(data.percent_positive, data.review_count); const earlyAccessClass = data.is_early_access ? 'early-access-yes' : 'early-access-no'; let languageSupportRussianText = "Отсутствует"; let languageSupportRussianClass = 'language-no'; if (data.language_support_russian) { languageSupportRussianText = ""; if (data.language_support_russian.supported) languageSupportRussianText += "
Интерфейс: ✔ "; if (data.language_support_russian.full_audio) languageSupportRussianText += "
Озвучка: ✔ "; if (data.language_support_russian.subtitles) languageSupportRussianText += "
Субтитры: ✔"; languageSupportRussianClass = languageSupportRussianText ? 'language-yes' : 'language-no'; } let languageSupportEnglishText = "Отсутствует"; let languageSupportEnglishClass = 'language-no'; if (data.language_support_english) { languageSupportEnglishText = ""; if (data.language_support_english.supported) languageSupportEnglishText += "
Интерфейс: ✔ "; if (data.language_support_english.full_audio) languageSupportEnglishText += "
Озвучка: ✔ "; if (data.language_support_english.subtitles) languageSupportEnglishText += "
Субтитры: ✔"; languageSupportEnglishClass = languageSupportEnglishText ? 'language-yes' : 'language-no'; } return `
Серия игр: ${data.franchises || "Нет данных"}
Отзывы: ${data.percent_positive || "0"}% (${data.review_count || "0"})
Ранний доступ: ${data.is_early_access ? "Да" : "Нет"}
Русский язык: ${languageSupportRussianText}
${scriptsConfig.toggleEnglishLangInfo ? `
Английский язык: ${languageSupportEnglishText}
` : ''}
Описание: ${data.short_description || "Нет данных"}
`; } function getReviewClass(percent, totalReviews) { if (totalReviews === 0) return 'no-reviews'; if (percent >= 70) return 'positive'; if (percent >= 40) return 'mixed'; return 'negative'; } function init() { const style = document.createElement('style'); style.textContent = styles; document.head.append(style); const header = document.querySelector('.header-title'); if (header) { header.parentNode.insertBefore(createFiltersContainer(), header.nextElementSibling); } document.addEventListener('click', (e) => { if (e.target.closest('.steamdb-enhancer')) { handleFilterClick(e); handleControlClick(e); } }); document.querySelector('#process-btn').addEventListener('click', () => { if (!isProcessingStarted) { isProcessingStarted = true; document.querySelector('#process-btn').textContent = PROCESS_BUTTON_TEXT.processing; new MutationObserver(collectAppIds).observe(document.body, { childList: true, subtree: true }); collectAppIds(); } }); document.addEventListener('mouseover', handleHover); } init(); })();