您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a button in steam inventory for stack items
// ==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(); } } })();