// ==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 = `
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();
})();