// ==UserScript==
// @name Steam Stack Inventory
// @namespace https://github.com/Kostya12rus/steam_inventory_stack/
// @supportURL https://github.com/Kostya12rus/steam_inventory_stack/issues
// @version 1.0.1
// @description Add a button in steam inventory for stack items
// @author Kostya12rus
// @match https://steamcommunity.com/profiles/*/inventory*
// @match https://steamcommunity.com/id/*/inventory*
// @license AGPL-3.0
// ==/UserScript==
(function() {
'use strict';
createButton();
// Функция для создания кнопки
function createButton() {
const userSteamID = g_steamID;
let { m_steamid } = g_ActiveInventory;
let inProgress = false;
if (userSteamID !== m_steamid) return;
const button = document.createElement("button");
button.innerText = "Stack Inventory";
button.classList.add("btn_darkblue_white_innerfade");
button.style.width = "100%";
button.style.height = "30px";
button.style.lineHeight = "30px";
button.style.fontSize = "15px";
button.style.position = "relative";
button.style.zIndex = "2";
// Добавляем обработчик события клика
button.addEventListener("click", async function() {
if (inProgress) return;
inProgress = true;
await startStackInventory()
inProgress = false;
});
async function stackItem(item, leaderItem, token) {
const { amount, id: fromitemid } = item;
const { id: destitemid } = leaderItem;
const {m_appid, m_steamid} = g_ActiveInventory;
const steamToken = token;
const url = 'https://api.steampowered.com/IInventoryService/CombineItemStacks/v1/';
const data = {
'access_token': steamToken,
'appid': m_appid,
'fromitemid': fromitemid,
'destitemid': destitemid,
'quantity': amount,
'steamid': m_steamid,
};
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(data).toString()
});
} catch (error) {
// логирование ошибки, если необходимо
}
}
async function startStackInventory() {
let token = document.querySelector("#application_config")?.getAttribute("data-loyalty_webapi_token");
if (token) {
token = token.replace(/"/g, "");
}
else {
return;
}
const inventory = await getFullInventory();
const totalItems = Object.values(inventory).reduce((sum, instanceDict) => {
return sum + Object.values(instanceDict).reduce((instanceSum, items) => {
return instanceSum + items.length - 1; // -1 для исключения leaderItem
}, 0);
}, 0);
if (totalItems < 2) {
alert("Недостаточно предметов для объединения, либо не удалось получить список предметов. Пожалуйста, попробуйте позже");
return;
}
let processedItems = 0;
const progressModal = createProgressModal(totalItems);
for (const classid in inventory) {
if (inventory.hasOwnProperty(classid)) {
const instanceDict = inventory[classid];
for (const instanceid in instanceDict) {
if (instanceDict.hasOwnProperty(instanceid)) {
const items = instanceDict[instanceid];
if (items.length < 2) continue;
let leaderItem;
if (instanceid === "0") {
leaderItem = items[0];
} else {
leaderItem = items[items.length - 1];
}
for (const item of items) {
if (item === leaderItem) continue;
stackItem(item, leaderItem, token);
processedItems++;
updateProgressModal(progressModal, processedItems, totalItems);
await new Promise(resolve => setTimeout(resolve, 75));
}
}
}
}
}
startCountdownAndClose(progressModal.overlay, progressModal.modal, progressModal.countdownText);
}
// Функция для создания модального окна
function createProgressModal(totalItems) {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
overlay.style.zIndex = '9999';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.transition = 'opacity 0.3s ease-in-out';
overlay.style.opacity = '0';
const modal = document.createElement('div');
modal.style.padding = '30px';
modal.style.backgroundColor = '#242424'; // Темно-серый цвет с легким оттенком
modal.style.borderRadius = '12px';
modal.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.5)';
modal.style.textAlign = 'center';
modal.style.color = '#e0e0e0'; // Светло-серый цвет для текста
modal.style.width = '500px';
modal.style.transition = 'transform 0.3s ease-in-out, opacity 0.3s ease-in-out';
modal.style.transform = 'scale(0.9)';
modal.style.opacity = '0';
const title = document.createElement('h3');
title.innerText = 'Stacking Inventory Items...';
title.style.marginBottom = '15px';
title.style.fontSize = '22px'; // Немного увеличен размер шрифта
title.style.fontWeight = 'bold';
title.style.color = '#ffffff'; // Белый цвет для заголовка
modal.appendChild(title);
const progress = document.createElement('div');
progress.style.marginTop = '20px';
progress.style.position = 'relative';
modal.appendChild(progress);
const progressBar = document.createElement('div');
progressBar.style.width = '100%';
progressBar.style.height = '24px';
progressBar.style.backgroundColor = '#333'; // Более темный цвет фона
progressBar.style.borderRadius = '12px';
progressBar.style.overflow = 'hidden';
progressBar.style.position = 'relative'; // Добавлено позиционирование
progress.appendChild(progressBar);
const progressFill = document.createElement('div');
progressFill.style.height = '100%';
progressFill.style.width = '0%';
progressFill.style.backgroundColor = '#008cba'; // Синий цвет для прогресса
progressFill.style.transition = 'width 0.4s ease';
progressFill.style.borderRadius = '12px';
progressFill.style.position = 'absolute'; // Абсолютное позиционирование для заполнения
progressFill.style.top = '0';
progressFill.style.left = '0';
progressBar.appendChild(progressFill);
const progressBarText = document.createElement('span');
progressBarText.style.position = 'absolute';
progressBarText.style.top = '50%';
progressBarText.style.left = '50%';
progressBarText.style.transform = 'translate(-50%, -50%)';
progressBarText.style.fontSize = '14px';
progressBarText.style.color = '#ffffff';
progressBarText.style.zIndex = '1';
progressBarText.style.pointerEvents = 'none'; // Чтобы текст не перекрывал клики
progressBar.appendChild(progressBarText);
const progressText = document.createElement('div');
progressText.style.marginTop = '15px';
progressText.style.fontSize = '16px';
progressText.style.color = '#f0f0f0';
progressText.innerText = `0 of ${totalItems} items processed`;
modal.appendChild(progressText);
const countdownText = document.createElement('div');
countdownText.style.marginTop = '20px';
countdownText.style.fontSize = '16px';
countdownText.style.color = '#ffcc00'; // Желтый цвет для обратного отсчета
modal.appendChild(countdownText);
const closeButton = document.createElement('button');
closeButton.innerText = 'Close';
closeButton.style.marginTop = '25px';
closeButton.style.padding = '12px 24px';
closeButton.style.backgroundColor = '#008cba'; // Синий цвет для кнопки
closeButton.style.border = 'none';
closeButton.style.borderRadius = '8px';
closeButton.style.color = '#fff'; // Белый цвет для текста кнопки
closeButton.style.fontSize = '16px';
closeButton.style.cursor = 'pointer';
closeButton.style.transition = 'background-color 0.3s ease';
closeButton.onmouseover = () => {
closeButton.style.backgroundColor = '#0077a3'; // Более темный синий при наведении
};
closeButton.onmouseout = () => {
closeButton.style.backgroundColor = '#008cba'; // Возвращаем исходный цвет
};
closeButton.onclick = () => closeProgressModal(overlay);
modal.appendChild(closeButton);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Плавное появление оверлея и модального окна
requestAnimationFrame(() => {
overlay.style.opacity = '1';
modal.style.transform = 'scale(1)';
modal.style.opacity = '1';
});
return { modal, progressFill, progressText, countdownText, overlay, progressBarText };
}
// Функция для закрытия всплывающего окна с обратным отсчетом
function startCountdownAndClose(overlay, modal, countdownText) {
let countdown = 5;
const interval = setInterval(() => {
if (countdown > 0) {
countdownText.innerText = `Процесс завершен. Страница обновится через ${countdown} секунд...`;
countdown--;
} else {
clearInterval(interval);
closeProgressModal(overlay);
}
}, 1000);
}
// Функция для закрытия всплывающего окна
function closeProgressModal(overlay) {
document.body.removeChild(overlay);
window.location.reload();
}
// Функция для обновления прогресса
function updateProgressModal({ progressFill, progressText, progressBarText }, processedItems, totalItems) {
const progressPercentage = ((processedItems / totalItems) * 100).toFixed(1);
progressFill.style.width = `${progressPercentage}%`;
const timeLeft = (((totalItems-processedItems)*0.075).toFixed(1));
progressBarText.innerText = `${progressPercentage}% (~${timeLeft} sec)`;
progressText.innerText = `${processedItems} of ${totalItems} items processed`;
}
async function getFullInventory() {
try {
const inventoryItems = await getInventoryItems();
const itemDict = {};
for (const itemData of inventoryItems) {
for (const item of Object.values(itemData)) {
const { classid, instanceid } = item;
if (!itemDict[classid]) {
itemDict[classid] = {};
}
if (!itemDict[classid][instanceid]) {
itemDict[classid][instanceid] = [];
}
itemDict[classid][instanceid].push(item);
}
}
return itemDict;
} catch (error) {
console.error("Ошибка при получении предметов инвентаря:", error);
}
return {};
}
function getInventoryItems(start = 0, inventoryItems = []) {
const {m_appid, m_contextid, m_steamid} = g_ActiveInventory;
const url = `https://steamcommunity.com/profiles/${m_steamid}/inventory/json/${m_appid}/${m_contextid}/?start=${start}`;
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data.success) {
throw new Error("Не удалось получить данные инвентаря.");
}
inventoryItems = inventoryItems.concat(data.rgInventory || []);
if (data.more) {
const more_start = data.more_start || 0;
if (Number.isInteger(more_start) && more_start > 0) {
return getInventoryItems(more_start, inventoryItems);
}
}
return inventoryItems;
})
.catch(error => {
console.error("Ошибка проверки инвентаря:", error);
throw error;
});
}
// Функция для обновления текста кнопки с логированием
function updateButtonText() {
const gameNameElement = document.querySelector('.name_game');
if (gameNameElement) {
button.innerText = "Stack Inventory " + gameNameElement.textContent.trim();
}
}
// Функция для ожидания появления элемента
function waitForElement(selector) {
return new Promise((resolve) => {
const observer = new MutationObserver((mutations, observer) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// Наблюдатель для изменений в элементе с классом name_game с логированием
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
updateButtonText();
}
});
});
// Настройка наблюдателя
waitForElement('.name_game').then((target) => {
observer.observe(target, { childList: true, subtree: true, characterData: true });
updateButtonText(); // Обновление текста кнопки сразу после установки наблюдателя
});
// Вставка кнопки с логированием
const referenceElement = document.querySelector('#tabcontent_inventory');
if (referenceElement) {
referenceElement.parentNode.insertBefore(button, referenceElement);
updateButtonText();
}
}
})();