Steam Stack Inventory

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