Steam Stack Inventory

Add a button in steam inventory for stack items

当前为 2024-08-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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