Steam Stack Inventory

Add a button in steam inventory for stack items

  1. // ==UserScript==
  2. // @name Steam Stack Inventory
  3. // @namespace https://github.com/Kostya12rus/steam_inventory_stack/
  4. // @supportURL https://github.com/Kostya12rus/steam_inventory_stack/issues
  5. // @version 1.0.1
  6. // @description Add a button in steam inventory for stack items
  7. // @author Kostya12rus
  8. // @match https://steamcommunity.com/profiles/*/inventory*
  9. // @match https://steamcommunity.com/id/*/inventory*
  10. // @license AGPL-3.0
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15. createButton();
  16.  
  17. // Функция для создания кнопки
  18. function createButton() {
  19. const userSteamID = g_steamID;
  20. let { m_steamid } = g_ActiveInventory;
  21. let inProgress = false;
  22. if (userSteamID !== m_steamid) return;
  23.  
  24. const button = document.createElement("button");
  25. button.innerText = "Stack Inventory";
  26. button.classList.add("btn_darkblue_white_innerfade");
  27. button.style.width = "100%";
  28. button.style.height = "30px";
  29. button.style.lineHeight = "30px";
  30. button.style.fontSize = "15px";
  31. button.style.position = "relative";
  32. button.style.zIndex = "2";
  33.  
  34. // Добавляем обработчик события клика
  35. button.addEventListener("click", async function() {
  36. if (inProgress) return;
  37. inProgress = true;
  38. await startStackInventory()
  39. inProgress = false;
  40. });
  41. async function stackItem(item, leaderItem, token) {
  42. const { amount, id: fromitemid } = item;
  43. const { id: destitemid } = leaderItem;
  44. const {m_appid, m_steamid} = g_ActiveInventory;
  45. const steamToken = token;
  46. const url = 'https://api.steampowered.com/IInventoryService/CombineItemStacks/v1/';
  47.  
  48. const data = {
  49. 'access_token': steamToken,
  50. 'appid': m_appid,
  51. 'fromitemid': fromitemid,
  52. 'destitemid': destitemid,
  53. 'quantity': amount,
  54. 'steamid': m_steamid,
  55. };
  56. try {
  57. await fetch(url, {
  58. method: 'POST',
  59. headers: {
  60. 'Content-Type': 'application/x-www-form-urlencoded',
  61. },
  62. body: new URLSearchParams(data).toString()
  63. });
  64. } catch (error) {
  65. // логирование ошибки, если необходимо
  66. }
  67. }
  68. async function startStackInventory() {
  69. let token = document.querySelector("#application_config")?.getAttribute("data-loyalty_webapi_token");
  70. if (token) {
  71. token = token.replace(/"/g, "");
  72. }
  73. else {
  74. return;
  75. }
  76. const inventory = await getFullInventory();
  77. const totalItems = Object.values(inventory).reduce((sum, instanceDict) => {
  78. return sum + Object.values(instanceDict).reduce((instanceSum, items) => {
  79. return instanceSum + items.length - 1; // -1 для исключения leaderItem
  80. }, 0);
  81. }, 0);
  82. if (totalItems < 2) {
  83. alert("Недостаточно предметов для объединения, либо не удалось получить список предметов. Пожалуйста, попробуйте позже");
  84. return;
  85. }
  86.  
  87. let processedItems = 0;
  88. const progressModal = createProgressModal(totalItems);
  89.  
  90. for (const classid in inventory) {
  91. if (inventory.hasOwnProperty(classid)) {
  92. const instanceDict = inventory[classid];
  93. for (const instanceid in instanceDict) {
  94. if (instanceDict.hasOwnProperty(instanceid)) {
  95. const items = instanceDict[instanceid];
  96. if (items.length < 2) continue;
  97. let leaderItem;
  98. if (instanceid === "0") {
  99. leaderItem = items[0];
  100. } else {
  101. leaderItem = items[items.length - 1];
  102. }
  103. for (const item of items) {
  104. if (item === leaderItem) continue;
  105. stackItem(item, leaderItem, token);
  106. processedItems++;
  107. updateProgressModal(progressModal, processedItems, totalItems);
  108. await new Promise(resolve => setTimeout(resolve, 75));
  109. }
  110. }
  111. }
  112. }
  113. }
  114. startCountdownAndClose(progressModal.overlay, progressModal.modal, progressModal.countdownText);
  115. }
  116.  
  117. // Функция для создания модального окна
  118. function createProgressModal(totalItems) {
  119. const overlay = document.createElement('div');
  120. overlay.style.position = 'fixed';
  121. overlay.style.top = '0';
  122. overlay.style.left = '0';
  123. overlay.style.width = '100%';
  124. overlay.style.height = '100%';
  125. overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  126. overlay.style.zIndex = '9999';
  127. overlay.style.display = 'flex';
  128. overlay.style.justifyContent = 'center';
  129. overlay.style.alignItems = 'center';
  130. overlay.style.transition = 'opacity 0.3s ease-in-out';
  131. overlay.style.opacity = '0';
  132.  
  133. const modal = document.createElement('div');
  134. modal.style.padding = '30px';
  135. modal.style.backgroundColor = '#242424'; // Темно-серый цвет с легким оттенком
  136. modal.style.borderRadius = '12px';
  137. modal.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.5)';
  138. modal.style.textAlign = 'center';
  139. modal.style.color = '#e0e0e0'; // Светло-серый цвет для текста
  140. modal.style.width = '500px';
  141. modal.style.transition = 'transform 0.3s ease-in-out, opacity 0.3s ease-in-out';
  142. modal.style.transform = 'scale(0.9)';
  143. modal.style.opacity = '0';
  144.  
  145. const title = document.createElement('h3');
  146. title.innerText = 'Stacking Inventory Items...';
  147. title.style.marginBottom = '15px';
  148. title.style.fontSize = '22px'; // Немного увеличен размер шрифта
  149. title.style.fontWeight = 'bold';
  150. title.style.color = '#ffffff'; // Белый цвет для заголовка
  151. modal.appendChild(title);
  152.  
  153. const progress = document.createElement('div');
  154. progress.style.marginTop = '20px';
  155. progress.style.position = 'relative';
  156. modal.appendChild(progress);
  157.  
  158. const progressBar = document.createElement('div');
  159. progressBar.style.width = '100%';
  160. progressBar.style.height = '24px';
  161. progressBar.style.backgroundColor = '#333'; // Более темный цвет фона
  162. progressBar.style.borderRadius = '12px';
  163. progressBar.style.overflow = 'hidden';
  164. progressBar.style.position = 'relative'; // Добавлено позиционирование
  165. progress.appendChild(progressBar);
  166.  
  167. const progressFill = document.createElement('div');
  168. progressFill.style.height = '100%';
  169. progressFill.style.width = '0%';
  170. progressFill.style.backgroundColor = '#008cba'; // Синий цвет для прогресса
  171. progressFill.style.transition = 'width 0.4s ease';
  172. progressFill.style.borderRadius = '12px';
  173. progressFill.style.position = 'absolute'; // Абсолютное позиционирование для заполнения
  174. progressFill.style.top = '0';
  175. progressFill.style.left = '0';
  176. progressBar.appendChild(progressFill);
  177.  
  178. const progressBarText = document.createElement('span');
  179. progressBarText.style.position = 'absolute';
  180. progressBarText.style.top = '50%';
  181. progressBarText.style.left = '50%';
  182. progressBarText.style.transform = 'translate(-50%, -50%)';
  183. progressBarText.style.fontSize = '14px';
  184. progressBarText.style.color = '#ffffff';
  185. progressBarText.style.zIndex = '1';
  186. progressBarText.style.pointerEvents = 'none'; // Чтобы текст не перекрывал клики
  187. progressBar.appendChild(progressBarText);
  188.  
  189. const progressText = document.createElement('div');
  190. progressText.style.marginTop = '15px';
  191. progressText.style.fontSize = '16px';
  192. progressText.style.color = '#f0f0f0';
  193. progressText.innerText = `0 of ${totalItems} items processed`;
  194. modal.appendChild(progressText);
  195.  
  196. const countdownText = document.createElement('div');
  197. countdownText.style.marginTop = '20px';
  198. countdownText.style.fontSize = '16px';
  199. countdownText.style.color = '#ffcc00'; // Желтый цвет для обратного отсчета
  200. modal.appendChild(countdownText);
  201.  
  202. const closeButton = document.createElement('button');
  203. closeButton.innerText = 'Close';
  204. closeButton.style.marginTop = '25px';
  205. closeButton.style.padding = '12px 24px';
  206. closeButton.style.backgroundColor = '#008cba'; // Синий цвет для кнопки
  207. closeButton.style.border = 'none';
  208. closeButton.style.borderRadius = '8px';
  209. closeButton.style.color = '#fff'; // Белый цвет для текста кнопки
  210. closeButton.style.fontSize = '16px';
  211. closeButton.style.cursor = 'pointer';
  212. closeButton.style.transition = 'background-color 0.3s ease';
  213.  
  214. closeButton.onmouseover = () => {
  215. closeButton.style.backgroundColor = '#0077a3'; // Более темный синий при наведении
  216. };
  217.  
  218. closeButton.onmouseout = () => {
  219. closeButton.style.backgroundColor = '#008cba'; // Возвращаем исходный цвет
  220. };
  221.  
  222. closeButton.onclick = () => closeProgressModal(overlay);
  223.  
  224. modal.appendChild(closeButton);
  225.  
  226. overlay.appendChild(modal);
  227. document.body.appendChild(overlay);
  228.  
  229. // Плавное появление оверлея и модального окна
  230. requestAnimationFrame(() => {
  231. overlay.style.opacity = '1';
  232. modal.style.transform = 'scale(1)';
  233. modal.style.opacity = '1';
  234. });
  235.  
  236. return { modal, progressFill, progressText, countdownText, overlay, progressBarText };
  237. }
  238.  
  239.  
  240. // Функция для закрытия всплывающего окна с обратным отсчетом
  241. function startCountdownAndClose(overlay, modal, countdownText) {
  242. let countdown = 5;
  243.  
  244. const interval = setInterval(() => {
  245. if (countdown > 0) {
  246. countdownText.innerText = `Процесс завершен. Страница обновится через ${countdown} секунд...`;
  247. countdown--;
  248. } else {
  249. clearInterval(interval);
  250. closeProgressModal(overlay);
  251. }
  252. }, 1000);
  253. }
  254.  
  255. // Функция для закрытия всплывающего окна
  256. function closeProgressModal(overlay) {
  257. document.body.removeChild(overlay);
  258. window.location.reload();
  259. }
  260.  
  261. // Функция для обновления прогресса
  262. function updateProgressModal({ progressFill, progressText, progressBarText }, processedItems, totalItems) {
  263. const progressPercentage = ((processedItems / totalItems) * 100).toFixed(1);
  264. progressFill.style.width = `${progressPercentage}%`;
  265. const timeLeft = (((totalItems-processedItems)*0.075).toFixed(1));
  266. progressBarText.innerText = `${progressPercentage}% (~${timeLeft} sec)`;
  267. progressText.innerText = `${processedItems} of ${totalItems} items processed`;
  268. }
  269.  
  270. async function getFullInventory() {
  271. try {
  272. const inventoryItems = await getInventoryItems();
  273. const itemDict = {};
  274. for (const itemData of inventoryItems) {
  275. for (const item of Object.values(itemData)) {
  276. const { classid, instanceid } = item;
  277. if (!itemDict[classid]) {
  278. itemDict[classid] = {};
  279. }
  280. if (!itemDict[classid][instanceid]) {
  281. itemDict[classid][instanceid] = [];
  282. }
  283. itemDict[classid][instanceid].push(item);
  284. }
  285. }
  286. return itemDict;
  287. } catch (error) {
  288. console.error("Ошибка при получении предметов инвентаря:", error);
  289. }
  290. return {};
  291. }
  292. function getInventoryItems(start = 0, inventoryItems = []) {
  293. const {m_appid, m_contextid, m_steamid} = g_ActiveInventory;
  294. const url = `https://steamcommunity.com/profiles/${m_steamid}/inventory/json/${m_appid}/${m_contextid}/?start=${start}`;
  295.  
  296. return fetch(url, {
  297. method: 'GET',
  298. headers: {
  299. 'Content-Type': 'application/json'
  300. }
  301. })
  302. .then(response => {
  303. if (!response.ok) {
  304. throw new Error(`HTTP error! status: ${response.status}`);
  305. }
  306. return response.json();
  307. })
  308. .then(data => {
  309. if (!data.success) {
  310. throw new Error("Не удалось получить данные инвентаря.");
  311. }
  312. inventoryItems = inventoryItems.concat(data.rgInventory || []);
  313. if (data.more) {
  314. const more_start = data.more_start || 0;
  315. if (Number.isInteger(more_start) && more_start > 0) {
  316. return getInventoryItems(more_start, inventoryItems);
  317. }
  318. }
  319. return inventoryItems;
  320. })
  321. .catch(error => {
  322. console.error("Ошибка проверки инвентаря:", error);
  323. throw error;
  324. });
  325. }
  326.  
  327.  
  328. // Функция для обновления текста кнопки с логированием
  329. function updateButtonText() {
  330. const gameNameElement = document.querySelector('.name_game');
  331. if (gameNameElement) {
  332. button.innerText = "Stack Inventory " + gameNameElement.textContent.trim();
  333. }
  334. }
  335.  
  336. // Функция для ожидания появления элемента
  337. function waitForElement(selector) {
  338. return new Promise((resolve) => {
  339. const observer = new MutationObserver((mutations, observer) => {
  340. if (document.querySelector(selector)) {
  341. observer.disconnect();
  342. resolve(document.querySelector(selector));
  343. }
  344. });
  345. observer.observe(document.body, { childList: true, subtree: true });
  346. });
  347. }
  348.  
  349. // Наблюдатель для изменений в элементе с классом name_game с логированием
  350. const observer = new MutationObserver((mutations) => {
  351. mutations.forEach((mutation) => {
  352. if (mutation.type === 'childList' || mutation.type === 'characterData') {
  353. updateButtonText();
  354. }
  355. });
  356. });
  357.  
  358. // Настройка наблюдателя
  359. waitForElement('.name_game').then((target) => {
  360. observer.observe(target, { childList: true, subtree: true, characterData: true });
  361. updateButtonText(); // Обновление текста кнопки сразу после установки наблюдателя
  362. });
  363.  
  364. // Вставка кнопки с логированием
  365. const referenceElement = document.querySelector('#tabcontent_inventory');
  366. if (referenceElement) {
  367. referenceElement.parentNode.insertBefore(button, referenceElement);
  368. updateButtonText();
  369. }
  370. }
  371. })();