Customizable Bazaar Filler

On click, auto-fills bazaar item quantities and prices based on your preferences

当前为 2025-02-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Customizable Bazaar Filler
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description On click, auto-fills bazaar item quantities and prices based on your preferences
  6. // @match https://www.torn.com/bazaar.php*
  7. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_registerMenuCommand
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const styleBlock = `
  17. .item-toggle {
  18. position: absolute;
  19. width: 16px;
  20. height: 16px;
  21. top: 50%;
  22. right: 10px;
  23. transform: translateY(-50%);
  24. cursor: pointer;
  25. border-radius: 3px;
  26. -webkit-appearance: none;
  27. -moz-appearance: none;
  28. appearance: none;
  29. outline: none;
  30. }
  31. .item-toggle::after {
  32. content: '\\2713';
  33. position: absolute;
  34. font-size: 12px;
  35. top: 50%;
  36. left: 50%;
  37. transform: translate(-50%, -50%);
  38. display: none;
  39. }
  40. .item-toggle:checked::after {
  41. display: block;
  42. }
  43. /* Light mode */
  44. body:not(.dark-mode) .item-toggle {
  45. border: 1px solid #ccc;
  46. background: #fff;
  47. }
  48. body:not(.dark-mode) .item-toggle:checked {
  49. background: #007bff;
  50. }
  51. body:not(.dark-mode) .item-toggle:checked::after {
  52. color: #fff;
  53. }
  54. /* Dark mode */
  55. body.dark-mode .item-toggle {
  56. border: 1px solid #4e535a;
  57. background: #2f3237;
  58. }
  59. body.dark-mode .item-toggle:checked {
  60. background: #4e535a;
  61. }
  62. body.dark-mode .item-toggle:checked::after {
  63. color: #fff;
  64. }
  65.  
  66. /* Modal overlay */
  67. .settings-modal-overlay {
  68. position: fixed;
  69. top: 0; left: 0;
  70. width: 100%; height: 100%;
  71. background: rgba(0,0,0,0.5);
  72. z-index: 9999;
  73. display: flex;
  74. align-items: center;
  75. justify-content: center;
  76. }
  77. /* Modal container */
  78. .settings-modal {
  79. background: #fff;
  80. padding: 20px;
  81. border-radius: 8px;
  82. min-width: 300px;
  83. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  84. color: #000;
  85. }
  86. .settings-modal h2 {
  87. margin-top: 0;
  88. }
  89. .settings-modal label {
  90. display: block;
  91. margin: 10px 0 5px;
  92. }
  93. .settings-modal input, .settings-modal select {
  94. width: 100%;
  95. padding: 5px;
  96. box-sizing: border-box;
  97. }
  98. .settings-modal button {
  99. margin-top: 15px;
  100. padding: 5px 10px;
  101. }
  102. /* Button group alignment */
  103. .settings-modal div[style*="text-align:right"] {
  104. text-align: right;
  105. }
  106. /* Dark mode modal overrides */
  107. body.dark-mode .settings-modal {
  108. background: #2f3237;
  109. color: #fff;
  110. box-shadow: 0 2px 10px rgba(0,0,0,0.7);
  111. }
  112. body.dark-mode .settings-modal input,
  113. body.dark-mode .settings-modal select {
  114. background: #3c3f41;
  115. color: #fff;
  116. border: 1px solid #555;
  117. }
  118. body.dark-mode .settings-modal button {
  119. background: #555;
  120. color: #fff;
  121. border: none;
  122. }
  123. `;
  124. $('<style>').prop('type', 'text/css').html(styleBlock).appendTo('head');
  125.  
  126. let apiKey = GM_getValue("tornApiKey", "");
  127. let pricingSource = GM_getValue("pricingSource", "Market Value");
  128. let itemMarketOffset = GM_getValue("itemMarketOffset", -1);
  129. let itemMarketMarginType = GM_getValue("itemMarketMarginType", "absolute");
  130. let itemMarketListing = GM_getValue("itemMarketListing", 1);
  131. let itemMarketClamp = GM_getValue("itemMarketClamp", false);
  132. let marketMarginOffset = GM_getValue("marketMarginOffset", 0);
  133. let marketMarginType = GM_getValue("marketMarginType", "absolute");
  134.  
  135. const validPages = ["#/add", "#/manage"];
  136. let currentPage = window.location.hash;
  137.  
  138. let itemMarketCache = {};
  139.  
  140. const inputEvent = new Event("input", { bubbles: true });
  141. const keyupEvent = new Event("keyup", { bubbles: true });
  142.  
  143. function getItemIdByName(itemName) {
  144. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  145. for (let [id, info] of Object.entries(storedItems)) {
  146. if (info.name === itemName) return id;
  147. }
  148. return null;
  149. }
  150.  
  151.  
  152. function getPriceColor(listedPrice, marketValue) {
  153. if (marketValue <= 0) {
  154. return "#FFFFFF";
  155. }
  156. const ratio = listedPrice / marketValue;
  157.  
  158. if (ratio < 0) return "#FF0000";
  159. if (ratio > 2) return "#008000";
  160.  
  161. if (ratio < 1) {
  162. let t = Math.max(0, ratio);
  163. let r1 = 255, g1 = 0, b1 = 0;
  164. let r2 = 255, g2 = 255, b2 = 255;
  165. let r = Math.round(r1 + (r2 - r1) * t);
  166. let g = Math.round(g1 + (g2 - g1) * t);
  167. let b = Math.round(b1 + (b2 - b1) * t);
  168. return `rgb(${r},${g},${b})`;
  169. } else {
  170. let t = ratio - 1; // 0..1
  171. let r1 = 255, g1 = 255, b1 = 255;
  172. let r2 = 0, g2 = 128, b2 = 0;
  173. let r = Math.round(r1 + (r2 - r1) * t);
  174. let g = Math.round(g1 + (g2 - g1) * t);
  175. let b = Math.round(b1 + (b2 - b1) * t);
  176. return `rgb(${r},${g},${b})`;
  177. }
  178. }
  179.  
  180.  
  181.  
  182. async function fetchItemMarketData(itemId) {
  183. if (!apiKey) {
  184. console.error("No API key set for Item Market calls.");
  185. alert("No API key set. Please enter one in Settings first.");
  186. return null;
  187. }
  188. const now = Date.now();
  189. if (itemMarketCache[itemId] && (now - itemMarketCache[itemId].time < 30000)) {
  190. return itemMarketCache[itemId].data;
  191. }
  192. const url = `https://api.torn.com/v2/market/${itemId}/itemmarket`;
  193. try {
  194. const res = await fetch(url, {
  195. headers: { 'Authorization': 'ApiKey ' + apiKey }
  196. });
  197. const data = await res.json();
  198. if (data.error) {
  199. console.error("Item Market API error:", data.error);
  200. alert("Item Market API error: " + data.error.error);
  201. return null;
  202. }
  203. itemMarketCache[itemId] = { time: now, data };
  204. return data;
  205. } catch (err) {
  206. console.error("Failed fetching Item Market data:", err);
  207. alert("Failed to fetch Item Market data. Check your API key or try again later.");
  208. return null;
  209. }
  210. }
  211.  
  212. async function updateAddRow($row, isChecked) {
  213. const $qtyInput = $row.find(".amount input").first();
  214. const $priceInput = $row.find(".price input").first();
  215. if (!isChecked) {
  216. // Reset fields
  217. if ($qtyInput.data("orig") !== undefined) {
  218. $qtyInput.val($qtyInput.data("orig"));
  219. $qtyInput.removeData("orig");
  220. } else {
  221. $qtyInput.val("");
  222. }
  223. $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  224.  
  225. if ($priceInput.data("orig") !== undefined) {
  226. $priceInput.val($priceInput.data("orig"));
  227. $priceInput.removeData("orig");
  228. $priceInput.css("color", "");
  229. } else {
  230. $priceInput.val("");
  231. }
  232. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  233. return;
  234. }
  235.  
  236. if (!$qtyInput.data("orig")) $qtyInput.data("orig", $qtyInput.val());
  237. if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());
  238.  
  239. const itemName = $row.find(".name-wrap span.t-overflow").text().trim();
  240. const itemId = getItemIdByName(itemName);
  241. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  242. const matchedItem = Object.values(storedItems).find(i => i.name === itemName);
  243.  
  244. if (pricingSource === "Market Value" && matchedItem) {
  245. let qty = $row.find(".item-amount.qty").text().trim();
  246. $qtyInput.val(qty).trigger("keyup");
  247. let mv = Number(matchedItem.market_value);
  248. let finalPrice = mv;
  249. if (marketMarginType === "absolute") {
  250. finalPrice += marketMarginOffset;
  251. } else if (marketMarginType === "percentage") {
  252. finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
  253. }
  254. $priceInput.val(finalPrice.toLocaleString()).trigger("input");
  255. $priceInput.css("color", getPriceColor(finalPrice, mv));
  256. }
  257.  
  258. else if (pricingSource === "Item Market" && itemId) {
  259. const data = await fetchItemMarketData(itemId);
  260. if (!data || !data.itemmarket?.listings?.length) return;
  261. let listings = data.itemmarket.listings;
  262. const $checkbox = $row.find(".item-toggle").first();
  263. const listingsText = listings.slice(0, 5)
  264. .map((x, i) => `${i + 1}) $${x.price.toLocaleString()} x${x.amount}`)
  265. .join('\n');
  266. $checkbox.attr("title", listingsText);
  267. setTimeout(() => {
  268. $checkbox.removeAttr("title");
  269. }, 30000);
  270.  
  271. let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
  272. let listingPrice = listings[baseIndex].price;
  273. let finalPrice;
  274. if (itemMarketMarginType === "absolute") {
  275. finalPrice = listingPrice + Number(itemMarketOffset);
  276. } else if (itemMarketMarginType === "percentage") {
  277. finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
  278. }
  279. if (itemMarketClamp && matchedItem && matchedItem.market_value) {
  280. finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
  281. }
  282.  
  283. $qtyInput.val($row.find(".item-amount.qty").text().trim()).trigger("keyup");
  284. $priceInput.val(finalPrice.toLocaleString()).trigger("input");
  285.  
  286. if (matchedItem && matchedItem.market_value) {
  287. let marketVal = Number(matchedItem.market_value);
  288. $priceInput.css("color", getPriceColor(finalPrice, marketVal));
  289. }
  290. }
  291. else if (pricingSource === "Bazaars/TornPal") {
  292. alert("Bazaars/TornPal is not available. Please select another source.");
  293. }
  294. }
  295.  
  296. async function updateManageRow($row, isChecked) {
  297. const $priceInput = $row.find(".price___DoKP7 .input-money-group.success input.input-money").first();
  298. if (!isChecked) {
  299. // Reset fields
  300. if ($priceInput.data("orig") !== undefined) {
  301. $priceInput.val($priceInput.data("orig"));
  302. $priceInput.removeData("orig");
  303. $priceInput.css("color", "");
  304. } else {
  305. $priceInput.val("");
  306. }
  307. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  308. return;
  309. }
  310.  
  311. if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());
  312.  
  313. const itemName = $row.find(".desc___VJSNQ b").text().trim();
  314. const itemId = getItemIdByName(itemName);
  315. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  316. const matchedItem = Object.values(storedItems).find(i => i.name === itemName);
  317.  
  318. if (pricingSource === "Market Value" && matchedItem) {
  319. let mv = Number(matchedItem.market_value);
  320. let finalPrice = mv;
  321. if (marketMarginType === "absolute") {
  322. finalPrice += marketMarginOffset;
  323. } else if (marketMarginType === "percentage") {
  324. finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
  325. }
  326. $priceInput.val(finalPrice.toLocaleString()).trigger("input");
  327. $priceInput.css("color", getPriceColor(finalPrice, mv));
  328. }
  329. else if (pricingSource === "Item Market" && itemId) {
  330. const data = await fetchItemMarketData(itemId);
  331. if (!data || !data.itemmarket?.listings?.length) return;
  332. let listings = data.itemmarket.listings;
  333. const $checkbox = $row.find(".item-toggle").first();
  334. const listingsText = listings.slice(0, 5)
  335. .map((x, i) => `${i + 1}) $${x.price.toLocaleString()} x${x.amount}`)
  336. .join('\n');
  337. $checkbox.attr("title", listingsText);
  338. setTimeout(() => {
  339. $checkbox.removeAttr("title");
  340. }, 30000);
  341.  
  342. let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
  343. let listingPrice = listings[baseIndex].price;
  344. let finalPrice;
  345. if (itemMarketMarginType === "absolute") {
  346. finalPrice = listingPrice + Number(itemMarketOffset);
  347. } else if (itemMarketMarginType === "percentage") {
  348. finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
  349. }
  350. if (itemMarketClamp && matchedItem && matchedItem.market_value) {
  351. finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
  352. }
  353.  
  354. $priceInput.val(finalPrice.toLocaleString()).trigger("input");
  355. if (matchedItem && matchedItem.market_value) {
  356. let marketVal = Number(matchedItem.market_value);
  357. $priceInput.css("color", getPriceColor(finalPrice, marketVal));
  358. }
  359. }
  360. else if (pricingSource === "Bazaars/TornPal") {
  361. alert("Bazaars/TornPal is not available. Please select another source.");
  362. }
  363. }
  364.  
  365.  
  366. function openSettingsModal() {
  367. $('.settings-modal-overlay').remove();
  368. const $overlay = $('<div class="settings-modal-overlay"></div>');
  369. const $modal = $(`
  370. <div class="settings-modal" style="width:400px; max-width:90%; font-family:Arial, sans-serif;">
  371. <h2 style="margin-bottom:6px;">Bazaar Filler Settings</h2>
  372. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  373. <div style="margin-bottom:15px;">
  374. <label for="api-key-input" style="font-weight:bold; display:block;">Torn API Key</label>
  375. <input id="api-key-input" type="text" placeholder="Enter API key" style="width:100%; padding:6px; box-sizing:border-box;" value="${apiKey || ''}">
  376. </div>
  377. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  378. <div style="margin-bottom:15px;">
  379. <label for="pricing-source-select" style="font-weight:bold; display:block;">Pricing Source</label>
  380. <select id="pricing-source-select" style="width:100%; padding:6px; box-sizing:border-box;">
  381. <option value="Market Value">Market Value</option>
  382. <option value="Bazaars/TornPal">Bazaars/TornPal</option>
  383. <option value="Item Market">Item Market</option>
  384. </select>
  385. </div>
  386. <div id="market-value-options" style="display:none; margin-bottom:15px;">
  387. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  388. <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Market Value Options</h3>
  389. <div style="margin-bottom:10px;">
  390. <label for="market-margin-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
  391. <input id="market-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${marketMarginOffset}">
  392. </div>
  393. <div style="margin-bottom:10px;">
  394. <label for="market-margin-type" style="display:block;">Margin Type</label>
  395. <select id="market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
  396. <option value="absolute">Absolute ($)</option>
  397. <option value="percentage">Percentage (%)</option>
  398. </select>
  399. </div>
  400. </div>
  401. <div id="item-market-options" style="display:none; margin-bottom:15px;">
  402. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  403. <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Item Market Options</h3>
  404. <div style="margin-bottom:10px;">
  405. <label for="item-market-listing" style="display:block;">Listing Index (1 = lowest, 2 = 2nd lowest, etc)</label>
  406. <input id="item-market-listing" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketListing}">
  407. </div>
  408. <div style="margin-bottom:10px;">
  409. <label for="item-market-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
  410. <input id="item-market-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketOffset}">
  411. </div>
  412. <div style="margin-bottom:10px;">
  413. <label for="item-market-margin-type" style="display:block;">Margin Type</label>
  414. <select id="item-market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
  415. <option value="absolute">Absolute ($)</option>
  416. <option value="percentage">Percentage (%)</option>
  417. </select>
  418. </div>
  419. <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
  420. <input id="item-market-clamp" type="checkbox" style="margin-right:5px;" ${itemMarketClamp ? "checked" : ""}>
  421. <label for="item-market-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
  422. </div>
  423. </div>
  424. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  425. <div style="text-align:right;">
  426. <button id="settings-save" style="margin-right:8px; padding:6px 10px; cursor:pointer;">Save</button>
  427. <button id="settings-cancel" style="padding:6px 10px; cursor:pointer;">Cancel</button>
  428. </div>
  429. </div>
  430. `);
  431. $overlay.append($modal);
  432. $('body').append($overlay);
  433.  
  434. // Set initial selections
  435. $('#pricing-source-select').val(pricingSource);
  436. $('#item-market-margin-type').val(itemMarketMarginType);
  437. $('#market-margin-type').val(marketMarginType);
  438.  
  439. function toggleFields() {
  440. let src = $('#pricing-source-select').val();
  441. $('#item-market-options').toggle(src === 'Item Market');
  442. $('#market-value-options').toggle(src === 'Market Value');
  443. }
  444. $('#pricing-source-select').change(toggleFields);
  445. toggleFields();
  446.  
  447. $('#settings-save').click(function() {
  448. apiKey = $('#api-key-input').val().trim();
  449. pricingSource = $('#pricing-source-select').val();
  450. if (pricingSource === "Bazaars/TornPal") {
  451. alert("Bazaars/TornPal is not available. Please select another source.");
  452. return;
  453. }
  454. if (pricingSource === "Market Value") {
  455. marketMarginOffset = Number($('#market-margin-offset').val() || 0);
  456. marketMarginType = $('#market-margin-type').val();
  457. GM_setValue("marketMarginOffset", marketMarginOffset);
  458. GM_setValue("marketMarginType", marketMarginType);
  459. }
  460. if (pricingSource === "Item Market") {
  461. itemMarketListing = Number($('#item-market-listing').val() || 1);
  462. itemMarketOffset = Number($('#item-market-offset').val() || -1);
  463. itemMarketMarginType = $('#item-market-margin-type').val();
  464. itemMarketClamp = $('#item-market-clamp').is(':checked');
  465. GM_setValue("itemMarketListing", itemMarketListing);
  466. GM_setValue("itemMarketOffset", itemMarketOffset);
  467. GM_setValue("itemMarketMarginType", itemMarketMarginType);
  468. GM_setValue("itemMarketClamp", itemMarketClamp);
  469. }
  470. GM_setValue("tornApiKey", apiKey);
  471. GM_setValue("pricingSource", pricingSource);
  472. $overlay.remove();
  473. });
  474. $('#settings-cancel').click(() => $overlay.remove());
  475. }
  476.  
  477. function addPricingSourceLink() {
  478. if (document.getElementById('pricing-source-button')) return;
  479. let linksContainer = document.querySelector('.linksContainer___LiOTN');
  480. if (!linksContainer) return;
  481.  
  482. let link = document.createElement('a');
  483. link.id = 'pricing-source-button';
  484. link.href = '#';
  485. link.className = 'linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9';
  486. link.target = '_self';
  487. link.rel = 'noreferrer';
  488.  
  489. const iconSpan = document.createElement('span');
  490. iconSpan.className = 'iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV';
  491. iconSpan.innerHTML = `
  492. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  493. <path d="M8 4.754a3.246 3.246 0 1 1 0 6.492 3.246 3.246 0 0 1 0-6.492zM5.754 8a2.246 2.246 0 1 0 4.492 0 2.246 2.246 0 0 0-4.492 0z"/>
  494. <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.433 2.54 2.54l.292-.16a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.433-.902 2.54-2.541l-.16-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.54-2.54l-.292.16a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.416 1.6.42 1.184 1.185l-.16.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.318.094a1.873 1.873 0 0 0-1.116 2.692l.16.292c.416.764-.42 1.6-1.185 1.184l-.291-.16a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.318a1.873 1.873 0 0 0-2.692-1.116l-.292.16c-.764.416-1.6-.42-1.184-1.185l.16-.292a1.873 1.873 0 0 0-1.116-2.692l-.318-.094c-.835-.246-.835-1.428 0-1.674l.318-.094a1.873 1.873 0 0 0 1.116-2.692l-.16-.292c-.416-.764.42-1.6 1.185-1.184l.292.16a1.873 1.873 0 0 0 2.693-1.116l.094-.318z"/>
  495. </svg>
  496. `;
  497. link.appendChild(iconSpan);
  498.  
  499. const textSpan = document.createElement('span');
  500. textSpan.className = 'linkTitle____NPyM';
  501. textSpan.textContent = 'Bazaar Filler Settings';
  502. link.appendChild(textSpan);
  503.  
  504. link.addEventListener('click', function(e) {
  505. e.preventDefault();
  506. openSettingsModal();
  507. });
  508. linksContainer.insertBefore(link, linksContainer.firstChild);
  509. }
  510.  
  511. function addFillButtonAddPage() {
  512. if (pricingSource !== "Bazaars/TornPal") return;
  513. if ($("#fill-checked-items").length) return;
  514. let clearAction = $(".items-footer .clear-action");
  515. if (!clearAction.length) return;
  516.  
  517. let fillBtn = $('<span id="fill-checked-items" class="clear-action t-blue h c-pointer">Fill Checked Items (Disabled)</span>');
  518. clearAction.after(fillBtn);
  519. fillBtn.click(() => alert("Bazaars/TornPal is not available. Please select another source."));
  520. clearAction.off("click").on("click", () => $(".item-toggle").prop("checked", false));
  521. }
  522. function addUpdateButtonManagePage() {
  523. if (pricingSource !== "Bazaars/TornPal") return;
  524. if ($("#update-checked-items").length) return;
  525. let undoBtn = $(".confirmation___eWdQi .undo___FTgvP");
  526. if (!undoBtn.length) return;
  527.  
  528. let updateBtn = $('<button id="update-checked-items" type="button" style="margin-left:10px;">Update Checked (Disabled)</button>');
  529. undoBtn.after(updateBtn);
  530. updateBtn.click(() => alert("Bazaars/TornPal is not available. Please select another source."));
  531. }
  532.  
  533. function addAddPageCheckboxes() {
  534. $(".items-cont .title-wrap").each(function() {
  535. if ($(this).find(".item-toggle").length) return;
  536. $(this).css("position", "relative");
  537. const checkbox = $('<input>', {
  538. type: "checkbox",
  539. class: "item-toggle",
  540. click: async function(e) {
  541. e.stopPropagation();
  542. await updateAddRow($(this).closest("li.clearfix"), this.checked);
  543. }
  544. });
  545. $(this).append(checkbox);
  546. });
  547. }
  548.  
  549. function addManagePageCheckboxes() {
  550. $(".item___jLJcf").each(function() {
  551. const $desc = $(this).find(".desc___VJSNQ");
  552. if (!$desc.length || $desc.find(".item-toggle").length) return;
  553. $desc.css("position", "relative");
  554. const checkbox = $('<input>', {
  555. type: "checkbox",
  556. class: "item-toggle",
  557. click: async function(e) {
  558. e.stopPropagation();
  559. await updateManageRow($(this).closest(".item___jLJcf"), this.checked);
  560. }
  561. });
  562. $desc.append(checkbox);
  563. });
  564. }
  565.  
  566. if (!validPages.includes(currentPage)) return;
  567. const storedItems = localStorage.getItem("tornItems");
  568. const lastUpdated = GM_getValue("lastUpdated", "");
  569. const todayUTC = new Date().toISOString().split('T')[0];
  570.  
  571. if (apiKey && (!storedItems || lastUpdated !== todayUTC || new Date().getUTCHours() === 0)) {
  572. fetch(`https://api.torn.com/torn/?key=${apiKey}&selections=items`)
  573. .then(r => r.json())
  574. .then(data => {
  575. if (!data.items) {
  576. console.error("Failed to fetch Torn items or no items found. Possibly invalid API key or rate limit.");
  577. return;
  578. }
  579. let filtered = {};
  580. for (let [id, item] of Object.entries(data.items)) {
  581. if (item.tradeable) {
  582. filtered[id] = {
  583. name: item.name,
  584. market_value: item.market_value
  585. };
  586. }
  587. }
  588. localStorage.setItem("tornItems", JSON.stringify(filtered));
  589. GM_setValue("lastUpdated", todayUTC);
  590. })
  591. .catch(err => {
  592. console.error("Error fetching Torn items:", err);
  593. });
  594. }
  595.  
  596. const domObserver = new MutationObserver(() => {
  597. if (window.location.hash === "#/add") {
  598. addAddPageCheckboxes();
  599. addFillButtonAddPage();
  600. } else if (window.location.hash === "#/manage") {
  601. addManagePageCheckboxes();
  602. addUpdateButtonManagePage();
  603. }
  604. addPricingSourceLink();
  605. });
  606. domObserver.observe(document.body, { childList: true, subtree: true });
  607.  
  608. window.addEventListener('hashchange', () => {
  609. currentPage = window.location.hash;
  610. if (currentPage === "#/add") {
  611. addAddPageCheckboxes();
  612. addFillButtonAddPage();
  613. } else if (currentPage === "#/manage") {
  614. addManagePageCheckboxes();
  615. addUpdateButtonManagePage();
  616. }
  617. addPricingSourceLink();
  618. });
  619.  
  620. if (currentPage === "#/add") {
  621. addAddPageCheckboxes();
  622. addFillButtonAddPage();
  623. } else if (currentPage === "#/manage") {
  624. addManagePageCheckboxes();
  625. addUpdateButtonManagePage();
  626. }
  627. addPricingSourceLink();
  628. })();