Greasy Fork is available in English.

Customizable Bazaar Filler

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

  1. // ==UserScript==
  2. // @name Customizable Bazaar Filler
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.71
  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. // @grant GM_xmlhttpRequest
  12. // @connect tornpal.com
  13. // ==/UserScript==
  14.  
  15.  
  16. (function () {
  17. "use strict";
  18.  
  19. const styleBlock = `
  20. .item-toggle {
  21. width: 18px;
  22. height: 18px;
  23. border-radius: 4px;
  24. -webkit-appearance: none;
  25. -moz-appearance: none;
  26. appearance: none;
  27. outline: none;
  28. }
  29. @keyframes spin {
  30. from { transform: rotate(0deg); }
  31. to { transform: rotate(360deg); }
  32. }
  33. .item-toggle::after {
  34. content: '\\2713';
  35. position: absolute;
  36. font-size: 14px;
  37. top: 50%;
  38. left: 50%;
  39. transform: translate(-50%, -50%);
  40. display: none;
  41. }
  42. .item-toggle:checked::after {
  43. display: block;
  44. }
  45.  
  46. body:not(.dark-mode) .item-toggle {
  47. border: 1px solid #ccc;
  48. background: #fff;
  49. }
  50. body:not(.dark-mode) .item-toggle:checked {
  51. background: #007bff;
  52. }
  53. body:not(.dark-mode) .item-toggle:checked::after {
  54. color: #fff;
  55. }
  56.  
  57. body.dark-mode .item-toggle {
  58. border: 1px solid #4e535a;
  59. background: #2f3237;
  60. }
  61. body.dark-mode .item-toggle:checked {
  62. background: #4e535a;
  63. }
  64. body.dark-mode .item-toggle:checked::after {
  65. color: #fff;
  66. }
  67.  
  68. .checkbox-wrapper {
  69. position: absolute;
  70. top: 50%;
  71. right: 8px;
  72. width: 30px;
  73. height: 30px;
  74. transform: translateY(-50%);
  75. cursor: pointer;
  76. }
  77. .checkbox-wrapper input.item-toggle {
  78. position: absolute;
  79. top: 6px;
  80. left: 6px;
  81. }
  82.  
  83. .settings-modal-overlay {
  84. position: fixed;
  85. top: 0; left: 0;
  86. width: 100%; height: 100%;
  87. background: rgba(0,0,0,0.5);
  88. z-index: 9999;
  89. display: flex;
  90. align-items: center;
  91. justify-content: center;
  92. }
  93. .settings-modal {
  94. background: #fff;
  95. padding: 20px;
  96. border-radius: 8px;
  97. min-width: 300px;
  98. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  99. color: #000;
  100. }
  101. .settings-modal h2 {
  102. margin-top: 0;
  103. }
  104. .settings-modal label {
  105. display: block;
  106. margin: 10px 0 5px;
  107. }
  108. .settings-modal input, .settings-modal select {
  109. width: 100%;
  110. padding: 5px;
  111. box-sizing: border-box;
  112. }
  113. .settings-modal button {
  114. margin-top: 15px;
  115. padding: 5px 10px;
  116. }
  117. .settings-modal div[style*="text-align:right"] {
  118. text-align: right;
  119. }
  120. body.dark-mode .settings-modal {
  121. background: #2f3237;
  122. color: #fff;
  123. box-shadow: 0 2px 10px rgba(0,0,0,0.7);
  124. }
  125. body.dark-mode .settings-modal input,
  126. body.dark-mode .settings-modal select {
  127. background: #3c3f41;
  128. color: #fff;
  129. border: 1px solid #555;
  130. }
  131. body.dark-mode .settings-modal button {
  132. background: #555;
  133. color: #fff;
  134. border: none;
  135. }
  136.  
  137. /* Black Friday mode styling */
  138. .black-friday-active {
  139. color: #28a745 !important;
  140. }
  141. .black-friday-active .black-friday-icon {
  142. color: #28a745 !important;
  143. fill: #28a745 !important;
  144. }
  145. .black-friday-icon {
  146. color: inherit;
  147. fill: currentColor;
  148. }
  149. `;
  150. $("<style>")
  151. .prop("type", "text/css")
  152. .html(styleBlock)
  153. .appendTo("head");
  154.  
  155. let apiKey = GM_getValue("tornApiKey", "");
  156. let pricingSource = GM_getValue("pricingSource", "Market Value");
  157. let itemMarketOffset = GM_getValue("itemMarketOffset", -1);
  158. let itemMarketMarginType = GM_getValue("itemMarketMarginType", "absolute");
  159. let itemMarketListing = GM_getValue("itemMarketListing", 1);
  160. let itemMarketClamp = GM_getValue("itemMarketClamp", false);
  161. let marketMarginOffset = GM_getValue("marketMarginOffset", 0);
  162. let marketMarginType = GM_getValue("marketMarginType", "absolute");
  163. let bazaarCalcMethod = GM_getValue("bazaarCalcMethod", "cheapest");
  164. let bazaarMarginOffset = GM_getValue("bazaarMarginOffset", 0);
  165. let bazaarMarginType = GM_getValue("bazaarMarginType", "absolute");
  166. let bazaarClamp = GM_getValue("bazaarClamp", false);
  167. let bazaarListing = GM_getValue("bazaarListing", 1);
  168. let blackFridayMode = GM_getValue("blackFridayMode", false);
  169. const validPages = ["#/add", "#/manage"];
  170. let currentPage = window.location.hash;
  171. let itemMarketCache = {};
  172. let tornPalCache = { time: 0, data: null };
  173. let tornPalItemCache = {};
  174.  
  175. console.log("Initial Bazaar Settings:", {
  176. bazaarCalcMethod,
  177. bazaarMarginOffset,
  178. bazaarMarginType,
  179. bazaarClamp,
  180. bazaarListing
  181. });
  182.  
  183. function getItemIdByName(itemName) {
  184. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  185. for (const [id, info] of Object.entries(storedItems)) {
  186. if (info.name === itemName)
  187. return id;
  188. }
  189. return null;
  190. }
  191. function getPriceColor(listedPrice, marketValue) {
  192. if (marketValue <= 0)
  193. return "";
  194. const ratio = listedPrice / marketValue;
  195. const lowerBound = 0.998;
  196. const upperBound = 1.002;
  197. const isDarkMode = document.body.classList.contains("dark-mode");
  198. if (ratio >= lowerBound && ratio <= upperBound) {
  199. return "";
  200. }
  201. if (ratio < lowerBound) {
  202. const diff = lowerBound - ratio;
  203. const t = Math.min(diff / 0.05, 1.2);
  204. if (isDarkMode) {
  205. const r = Math.round(255 - t * (255 - 190));
  206. const g = Math.round(255 - t * (255 - 70));
  207. const b = Math.round(255 - t * (255 - 70));
  208. return `rgb(${r},${g},${b})`;
  209. }
  210. else {
  211. const r = Math.round(180 - t * 40);
  212. const g = Math.round(60 - t * 40);
  213. const b = Math.round(60 - t * 40);
  214. return `rgb(${r},${g},${b})`;
  215. }
  216. }
  217. else {
  218. const diff = ratio - upperBound;
  219. const t = Math.min(diff / 0.05, 1.2);
  220. if (isDarkMode) {
  221. const r = Math.round(255 - t * (255 - 70));
  222. const g = Math.round(255 - t * (255 - 190));
  223. const b = Math.round(255 - t * (255 - 70));
  224. return `rgb(${r},${g},${b})`;
  225. }
  226. else {
  227. const r = Math.round(60 - t * 40);
  228. const g = Math.round(160 - t * 40);
  229. const b = Math.round(60 - t * 40);
  230. return `rgb(${r},${g},${b})`;
  231. }
  232. }
  233. }
  234. async function fetchItemMarketData(itemId) {
  235. if (!apiKey) {
  236. console.error("No API key set for Item Market calls.");
  237. alert("No API key set. Please set your Torn API key in Bazaar Filler Settings before continuing.");
  238. return null;
  239. }
  240. const now = Date.now();
  241. if (itemMarketCache[itemId] && now - itemMarketCache[itemId].time < 30000) {
  242. return itemMarketCache[itemId].data;
  243. }
  244. const url = `https://api.torn.com/v2/market/${itemId}/itemmarket?comment=wBazaarFiller`;
  245. try {
  246. const res = await fetch(url, {
  247. headers: { Authorization: "ApiKey " + apiKey },
  248. });
  249. const data = await res.json();
  250. if (data.error) {
  251. console.error("Item Market API error:", data.error);
  252. alert("Item Market API error: " + data.error.error);
  253. return null;
  254. }
  255. itemMarketCache[itemId] = { time: now, data };
  256. return data;
  257. }
  258. catch (err) {
  259. console.error("Failed fetching Item Market data:", err);
  260. alert("Failed to fetch Item Market data. Check your API key or try again later.");
  261. return null;
  262. }
  263. }
  264. async function fetchTornPalData() {
  265. const now = Date.now();
  266. if (tornPalCache.data && now - tornPalCache.time < 60000) {
  267. return tornPalCache.data;
  268. }
  269. return new Promise((resolve, reject) => {
  270. GM_xmlhttpRequest({
  271. method: "GET",
  272. url: "https://tornpal.com/api/v1/markets/allprices?comment=wBazaarFiller",
  273. onload: function (response) {
  274. try {
  275. const data = JSON.parse(response.responseText);
  276. tornPalCache = { time: now, data };
  277. resolve(data);
  278. }
  279. catch (err) {
  280. console.error("Parsing error:", err);
  281. reject(err);
  282. }
  283. },
  284. onerror: function (err) {
  285. console.error("Failed fetching TornPal data:", err);
  286. reject(err);
  287. },
  288. });
  289. });
  290. }
  291. async function fetchTornPalItemData(itemId) {
  292. const now = Date.now();
  293. if (tornPalItemCache[itemId] && now - tornPalItemCache[itemId].time < 60000) {
  294. return tornPalItemCache[itemId].data;
  295. }
  296. return new Promise((resolve, reject) => {
  297. GM_xmlhttpRequest({
  298. method: "GET",
  299. url: `https://tornpal.com/api/v1/markets/clist/${itemId}?comment=wBazaarFiller`,
  300. onload: function (response) {
  301. try {
  302. const data = JSON.parse(response.responseText);
  303. tornPalItemCache[itemId] = { time: now, data };
  304. resolve(data);
  305. }
  306. catch (err) {
  307. console.error("Parsing error:", err);
  308. reject(err);
  309. }
  310. },
  311. onerror: function (err) {
  312. console.error("Failed fetching TornPal item data:", err);
  313. reject(err);
  314. },
  315. });
  316. });
  317. }
  318. function updatePriceFieldColor($priceInput) {
  319. var _a;
  320. let $row = $priceInput.closest("li.clearfix");
  321. let itemName = "";
  322. if ($row.length) {
  323. itemName = $row.find(".name-wrap span.t-overflow").text().trim();
  324. }
  325. else {
  326. $row = $priceInput.closest(".item___jLJcf");
  327. itemName = $row.length ? $row.find(".desc___VJSNQ b").text().trim() : "";
  328. }
  329. if (!itemName)
  330. return;
  331. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  332. const matchedItem = Object.values(storedItems).find((i) => i.name === itemName);
  333. if (!matchedItem || !matchedItem.market_value)
  334. return;
  335. const raw = ((_a = $priceInput.val()) === null || _a === void 0 ? void 0 : _a.replace(/,/g, "")) || "";
  336. const typedPrice = Number(raw);
  337. if (isNaN(typedPrice)) {
  338. $priceInput.css("color", "");
  339. return;
  340. }
  341. $priceInput.css("color", getPriceColor(typedPrice, matchedItem.market_value));
  342. }
  343. function attachPriceFieldObservers() {
  344. $(".price input").each(function () {
  345. const $el = $(this);
  346. if ($el.data("listenerAttached"))
  347. return;
  348. $el.on("input", function () {
  349. updatePriceFieldColor($(this));
  350. });
  351. $el.data("listenerAttached", true);
  352. updatePriceFieldColor($el);
  353. });
  354. $(".price___DoKP7 .input-money-group.success input.input-money").each(function () {
  355. const $el = $(this);
  356. if ($el.data("listenerAttached"))
  357. return;
  358. $el.on("input", function () {
  359. updatePriceFieldColor($(this));
  360. });
  361. $el.data("listenerAttached", true);
  362. updatePriceFieldColor($el);
  363. });
  364. $("[class*=bottomMobileMenu___] [class*=priceMobile___] .input-money-group.success input.input-money").each(function () {
  365. const $el = $(this);
  366. if ($el.data("listenerAttached"))
  367. return;
  368. $el.on("input", function () {
  369. updatePriceFieldColor($(this));
  370. });
  371. $el.data("listenerAttached", true);
  372. updatePriceFieldColor($el);
  373. });
  374. }
  375.  
  376. async function updateAddRow($row, isChecked) {
  377. var _a, _b;
  378. const $qtyInput = $row.find(".amount input").first();
  379. const $priceInput = $row.find(".price input").first();
  380. const $choiceCheckbox = $row.find("div.amount.choice-container input");
  381. if (!isChecked) {
  382. if ($choiceCheckbox.length && $choiceCheckbox.prop("checked")) {
  383. $choiceCheckbox.click();
  384. }
  385. if ($qtyInput.data("orig") !== undefined) {
  386. $qtyInput.val($qtyInput.data("orig"));
  387. $qtyInput.removeData("orig");
  388. }
  389. else {
  390. $qtyInput.val("");
  391. }
  392. $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  393. if ($priceInput.data("orig") !== undefined) {
  394. $priceInput.val($priceInput.data("orig"));
  395. $priceInput.removeData("orig");
  396. $priceInput.css("color", "");
  397. }
  398. else {
  399. $priceInput.val("");
  400. }
  401. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  402. $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  403. return;
  404. }
  405. if (!$qtyInput.data("orig"))
  406. $qtyInput.data("orig", $qtyInput.val());
  407. if (!$priceInput.data("orig"))
  408. $priceInput.data("orig", $priceInput.val());
  409. if (blackFridayMode) {
  410. if ($choiceCheckbox.length) {
  411. if (!$choiceCheckbox.prop("checked")) {
  412. $choiceCheckbox.click();
  413. }
  414. }
  415. else {
  416. const qty = $row.find(".item-amount.qty").text().trim();
  417. $qtyInput.val(qty);
  418. $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  419. }
  420. $priceInput.val("1");
  421. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  422. $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  423. return;
  424. }
  425. const itemName = $row.find(".name-wrap span.t-overflow").text().trim();
  426. const itemId = getItemIdByName(itemName);
  427. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  428. const matchedItem = Object.values(storedItems).find((i) => i.name === itemName);
  429. if ($choiceCheckbox.length) {
  430. if (!$choiceCheckbox.prop("checked")) {
  431. $choiceCheckbox.click();
  432. }
  433. }
  434. else {
  435. const qty = $row.find(".item-amount.qty").text().trim();
  436. $qtyInput.val(qty);
  437. $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  438. }
  439. if (pricingSource === "Market Value" && matchedItem) {
  440. const mv = matchedItem.market_value;
  441. let finalPrice = mv;
  442. if (marketMarginType === "absolute") {
  443. finalPrice += marketMarginOffset;
  444. }
  445. else if (marketMarginType === "percentage") {
  446. finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
  447. }
  448. $priceInput.val(finalPrice.toLocaleString("en-US"));
  449. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  450. $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  451. $priceInput.css("color", getPriceColor(finalPrice, mv));
  452. }
  453. else if (pricingSource === "Item Market" && itemId) {
  454. const data = await fetchItemMarketData(itemId);
  455. if (!data || !((_b = (_a = data.itemmarket) === null || _a === void 0 ? void 0 : _a.listings) === null || _b === void 0 ? void 0 : _b.length))
  456. return;
  457. const listings = data.itemmarket.listings;
  458. const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
  459. const listingsText = listings
  460. .slice(0, 5)
  461. .map((x, i) => `${i + 1}) $${x.price.toLocaleString("en-US")} x${x.amount}`)
  462. .join("\n");
  463. $checkbox.attr("title", listingsText);
  464. setTimeout(() => {
  465. $checkbox.removeAttr("title");
  466. }, 30000);
  467. const baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
  468. const listingPrice = listings[baseIndex].price;
  469. let finalPrice;
  470. if (itemMarketMarginType === "absolute") {
  471. finalPrice = listingPrice + itemMarketOffset;
  472. }
  473. else if (itemMarketMarginType === "percentage") {
  474. finalPrice = Math.round(listingPrice * (1 + itemMarketOffset / 100));
  475. }
  476. else {
  477. finalPrice = listingPrice;
  478. }
  479. if (itemMarketClamp && matchedItem && matchedItem.market_value) {
  480. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  481. }
  482. if (!$choiceCheckbox.length) {
  483. const qty = $row.find(".item-amount.qty").text().trim();
  484. $qtyInput.val(qty);
  485. $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  486. }
  487. else {
  488. if (!$choiceCheckbox.prop("checked")) {
  489. $choiceCheckbox.click();
  490. }
  491. }
  492. $priceInput.val(finalPrice.toLocaleString("en-US"));
  493. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  494. $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  495. if (matchedItem && matchedItem.market_value) {
  496. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  497. }
  498. }
  499. else if (pricingSource === "Bazaars/TornPal") {
  500. const data = await fetchTornPalData();
  501. if (!data || !data.items)
  502. return;
  503. if (!matchedItem)
  504. return;
  505. const tornPalItem = Object.values(data.items).find((item) => item.name === itemName);
  506. if (!tornPalItem)
  507. return;
  508. let basePrice;
  509. if (bazaarCalcMethod === "cheapest") {
  510. const itemId = getItemIdByName(itemName);
  511. if (itemId) {
  512. const itemData = await fetchTornPalItemData(itemId);
  513. if (itemData.listings && itemData.listings.length > 0) {
  514. const baseIndex = Math.min(bazaarListing - 1, itemData.listings.length - 1);
  515. basePrice = itemData.listings[baseIndex].price;
  516. } else {
  517. basePrice = tornPalItem.bazaar_cheapest;
  518. }
  519. } else {
  520. basePrice = tornPalItem.bazaar_cheapest;
  521. }
  522. }
  523. else {
  524. basePrice = tornPalItem.bazaar_weighted_average_5;
  525. }
  526. let finalPrice;
  527. if (bazaarMarginType === "absolute") {
  528. finalPrice = basePrice + bazaarMarginOffset;
  529. }
  530. else if (bazaarMarginType === "percentage") {
  531. finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
  532. }
  533. else {
  534. finalPrice = basePrice;
  535. }
  536. if (bazaarClamp && matchedItem.market_value) {
  537. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  538. }
  539. $priceInput.val(finalPrice.toLocaleString("en-US"));
  540. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  541. $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  542. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  543. }
  544. }
  545. async function updateManageRow($row, isChecked) {
  546. var _a, _b;
  547. const $priceInput = $row.find(".price___DoKP7 .input-money-group.success input.input-money").first();
  548.  
  549. if ($priceInput.length === 0) {
  550. console.warn("Price input not found in the row:", $row);
  551. return;
  552. }
  553.  
  554. if (!isChecked) {
  555. if ($priceInput.data("orig") !== undefined) {
  556. $priceInput.val($priceInput.data("orig"));
  557. $priceInput.removeData("orig");
  558. $priceInput.css("color", "");
  559. }
  560. else {
  561. $priceInput.val("");
  562. }
  563. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  564. return;
  565. }
  566. if (!$priceInput.data("orig"))
  567. $priceInput.data("orig", $priceInput.val());
  568. if (blackFridayMode) {
  569. $priceInput.val("1");
  570. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  571. return;
  572. }
  573. const itemName = $row.find(".desc___VJSNQ b").text().trim();
  574. const itemId = getItemIdByName(itemName);
  575. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  576. const matchedItem = Object.values(storedItems).find((i) => i.name === itemName);
  577. if (pricingSource === "Market Value" && matchedItem) {
  578. const mv = matchedItem.market_value;
  579. let finalPrice = mv;
  580. if (marketMarginType === "absolute") {
  581. finalPrice += marketMarginOffset;
  582. }
  583. else if (marketMarginType === "percentage") {
  584. finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
  585. }
  586. $priceInput.val(finalPrice.toLocaleString("en-US"));
  587. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  588. $priceInput.css("color", getPriceColor(finalPrice, mv));
  589. }
  590. else if (pricingSource === "Item Market" && itemId) {
  591. const data = await fetchItemMarketData(itemId);
  592. if (!data || !((_b = (_a = data.itemmarket) === null || _a === void 0 ? void 0 : _a.listings) === null || _b === void 0 ? void 0 : _b.length))
  593. return;
  594. const listings = data.itemmarket.listings;
  595. const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
  596. const listingsText = listings
  597. .slice(0, 5)
  598. .map((x, i) => `${i + 1}) $${x.price.toLocaleString("en-US")} x${x.amount}`)
  599. .join("\n");
  600. $checkbox.attr("title", listingsText);
  601. setTimeout(() => {
  602. $checkbox.removeAttr("title");
  603. }, 30000);
  604. const baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
  605. const listingPrice = listings[baseIndex].price;
  606. let finalPrice;
  607. if (itemMarketMarginType === "absolute") {
  608. finalPrice = listingPrice + itemMarketOffset;
  609. }
  610. else if (itemMarketMarginType === "percentage") {
  611. finalPrice = Math.round(listingPrice * (1 + itemMarketOffset / 100));
  612. }
  613. else {
  614. finalPrice = listingPrice;
  615. }
  616. if (itemMarketClamp && matchedItem && matchedItem.market_value) {
  617. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  618. }
  619. $priceInput.val(finalPrice.toLocaleString("en-US"));
  620. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  621. if (matchedItem && matchedItem.market_value) {
  622. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  623. }
  624. }
  625. else if (pricingSource === "Bazaars/TornPal") {
  626. const data = await fetchTornPalData();
  627. if (!data || !data.items)
  628. return;
  629. if (!matchedItem)
  630. return;
  631. const tornPalItem = Object.values(data.items).find((item) => item.name === itemName);
  632. if (!tornPalItem)
  633. return;
  634. let basePrice;
  635. if (bazaarCalcMethod === "cheapest") {
  636. const itemId = getItemIdByName(itemName);
  637. if (itemId) {
  638. const itemData = await fetchTornPalItemData(itemId);
  639. if (itemData.listings && itemData.listings.length > 0) {
  640. const baseIndex = Math.min(bazaarListing - 1, itemData.listings.length - 1);
  641. basePrice = itemData.listings[baseIndex].price;
  642. } else {
  643. basePrice = tornPalItem.bazaar_cheapest;
  644. }
  645. } else {
  646. basePrice = tornPalItem.bazaar_cheapest;
  647. }
  648. }
  649. else {
  650. basePrice = tornPalItem.bazaar_weighted_average_5;
  651. }
  652. let finalPrice;
  653. if (bazaarMarginType === "absolute") {
  654. finalPrice = basePrice + bazaarMarginOffset;
  655. }
  656. else if (bazaarMarginType === "percentage") {
  657. finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
  658. }
  659. else {
  660. finalPrice = basePrice;
  661. }
  662. if (bazaarClamp && matchedItem.market_value) {
  663. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  664. }
  665. $priceInput.val(finalPrice.toLocaleString("en-US"));
  666. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  667. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  668. }
  669. }
  670. async function updateManageRowMobile($row, isChecked) {
  671. var _a, _b;
  672. const $priceInput = $row
  673. .find("[class*=bottomMobileMenu___] [class*=priceMobile___] .input-money-group.success input.input-money")
  674. .first();
  675. if (!$priceInput.length) {
  676. console.error("Mobile price field not found.");
  677. return;
  678. }
  679. if (!isChecked) {
  680. if ($priceInput.data("orig") !== undefined) {
  681. $priceInput.val($priceInput.data("orig"));
  682. $priceInput.removeData("orig");
  683. $priceInput.css("color", "");
  684. }
  685. else {
  686. $priceInput.val("");
  687. }
  688. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  689. return;
  690. }
  691. if (!$priceInput.data("orig"))
  692. $priceInput.data("orig", $priceInput.val());
  693. if (blackFridayMode) {
  694. $priceInput.val("1");
  695. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  696. return;
  697. }
  698. const itemName = $row.find(".desc___VJSNQ b").text().trim();
  699. const itemId = getItemIdByName(itemName);
  700. const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
  701. const matchedItem = Object.values(storedItems).find((i) => i.name === itemName);
  702. if (pricingSource === "Market Value" && matchedItem) {
  703. const mv = matchedItem.market_value;
  704. let finalPrice = mv;
  705. if (marketMarginType === "absolute") {
  706. finalPrice += marketMarginOffset;
  707. }
  708. else if (marketMarginType === "percentage") {
  709. finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
  710. }
  711. $priceInput.val(finalPrice.toLocaleString("en-US"));
  712. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  713. $priceInput.css("color", getPriceColor(finalPrice, mv));
  714. }
  715. else if (pricingSource === "Item Market" && itemId) {
  716. const data = await fetchItemMarketData(itemId);
  717. if (!data || !((_b = (_a = data.itemmarket) === null || _a === void 0 ? void 0 : _a.listings) === null || _b === void 0 ? void 0 : _b.length))
  718. return;
  719. const listings = data.itemmarket.listings;
  720. const baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
  721. const listingPrice = listings[baseIndex].price;
  722. let finalPrice;
  723. if (itemMarketMarginType === "absolute") {
  724. finalPrice = listingPrice + itemMarketOffset;
  725. }
  726. else if (itemMarketMarginType === "percentage") {
  727. finalPrice = Math.round(listingPrice * (1 + itemMarketOffset / 100));
  728. }
  729. else {
  730. finalPrice = listingPrice;
  731. }
  732. if (itemMarketClamp && matchedItem && matchedItem.market_value) {
  733. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  734. }
  735. $priceInput.val(finalPrice.toLocaleString("en-US"));
  736. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  737. if (matchedItem && matchedItem.market_value) {
  738. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  739. }
  740. }
  741. else if (pricingSource === "Bazaars/TornPal") {
  742. const data = await fetchTornPalData();
  743. if (!data || !data.items)
  744. return;
  745. if (!matchedItem)
  746. return;
  747. const tornPalItem = Object.values(data.items).find((item) => item.name === itemName);
  748. if (!tornPalItem)
  749. return;
  750. let basePrice;
  751. if (bazaarCalcMethod === "cheapest") {
  752. const itemId = getItemIdByName(itemName);
  753. if (itemId) {
  754. const itemData = await fetchTornPalItemData(itemId);
  755. if (itemData.listings && itemData.listings.length > 0) {
  756. const baseIndex = Math.min(bazaarListing - 1, itemData.listings.length - 1);
  757. basePrice = itemData.listings[baseIndex].price;
  758. } else {
  759. basePrice = tornPalItem.bazaar_cheapest;
  760. }
  761. } else {
  762. basePrice = tornPalItem.bazaar_cheapest;
  763. }
  764. }
  765. else {
  766. basePrice = tornPalItem.bazaar_weighted_average_5;
  767. }
  768. let finalPrice;
  769. if (bazaarMarginType === "absolute") {
  770. finalPrice = basePrice + bazaarMarginOffset;
  771. }
  772. else if (bazaarMarginType === "percentage") {
  773. finalPrice = Math.round(basePrice * (1 + bazaarMarginOffset / 100));
  774. }
  775. else {
  776. finalPrice = basePrice;
  777. }
  778. if (bazaarClamp && matchedItem.market_value) {
  779. finalPrice = Math.max(finalPrice, matchedItem.market_value);
  780. }
  781. $priceInput.val(finalPrice.toLocaleString("en-US"));
  782. $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
  783. $priceInput.css("color", getPriceColor(finalPrice, matchedItem.market_value));
  784. }
  785. }
  786.  
  787. function openSettingsModal() {
  788. $(".settings-modal-overlay").remove();
  789. const $overlay = $('<div class="settings-modal-overlay"></div>');
  790. const $modal = $(`
  791. <div class="settings-modal" style="width:400px; max-width:90%; font-family:Arial, sans-serif;">
  792. <h2 style="margin-bottom:6px;">Bazaar Filler Settings</h2>
  793. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  794. <div style="margin-bottom:15px;">
  795. <label for="api-key-input" style="font-weight:bold; display:block;">Torn API Key</label>
  796. <div style="display:flex; align-items:center; gap:8px;">
  797. <input id="api-key-input" type="text" placeholder="Enter API key" style="flex:1; padding:6px; box-sizing:border-box;" value="${apiKey || ''}">
  798. <button id="refresh-market-values" style="padding:6px; cursor:pointer; background:none; border:none;" title="Refresh Market Values">
  799. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  800. <path d="M23 4v6h-6"></path>
  801. <path d="M1 20v-6h6"></path>
  802. <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
  803. </svg>
  804. </button>
  805. </div>
  806. </div>
  807. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  808. <div style="margin-bottom:15px;">
  809. <label for="pricing-source-select" style="font-weight:bold; display:block;">Pricing Source</label>
  810. <select id="pricing-source-select" style="width:100%; padding:6px; box-sizing:border-box;">
  811. <option value="Market Value">Market Value</option>
  812. <option value="Bazaars/TornPal">Bazaars/TornPal</option>
  813. <option value="Item Market">Item Market</option>
  814. </select>
  815. </div>
  816. <div id="market-value-options" style="display:none; margin-bottom:15px;">
  817. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  818. <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Market Value Options</h3>
  819. <div style="margin-bottom:10px;">
  820. <label for="market-margin-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
  821. <input id="market-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${marketMarginOffset}">
  822. </div>
  823. <div style="margin-bottom:10px;">
  824. <label for="market-margin-type" style="display:block;">Margin Type</label>
  825. <select id="market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
  826. <option value="absolute">Absolute ($)</option>
  827. <option value="percentage">Percentage (%)</option>
  828. </select>
  829. </div>
  830. </div>
  831. <div id="item-market-options" style="display:none; margin-bottom:15px;">
  832. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  833. <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Item Market Options</h3>
  834. <div style="margin-bottom:10px;">
  835. <label for="item-market-listing" style="display:block;">Listing Index (1 = lowest, 2 = 2nd lowest, etc)</label>
  836. <input id="item-market-listing" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketListing}">
  837. </div>
  838. <div style="margin-bottom:10px;">
  839. <label for="item-market-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
  840. <input id="item-market-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketOffset}">
  841. </div>
  842. <div style="margin-bottom:10px;">
  843. <label for="item-market-margin-type" style="display:block;">Margin Type</label>
  844. <select id="item-market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
  845. <option value="absolute">Absolute ($)</option>
  846. <option value="percentage">Percentage (%)</option>
  847. </select>
  848. </div>
  849. <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
  850. <input id="item-market-clamp" type="checkbox" style="margin-right:5px;" ${itemMarketClamp ? "checked" : ""}>
  851. <label for="item-market-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
  852. </div>
  853. </div>
  854. <div id="tornpal-options" style="display:none; margin-bottom:15px;">
  855. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  856. <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">TornPal Options</h3>
  857. <div style="margin-bottom:10px;">
  858. <label for="tornpal-calc-method" style="display:block;">Calculation Method</label>
  859. <select id="tornpal-calc-method" style="width:100%; padding:6px; box-sizing:border-box;">
  860. <option value="cheapest">Cheapest</option>
  861. <option value="average">Average</option>
  862. </select>
  863. </div>
  864. <div id="tornpal-listing-options" style="margin-bottom:10px;">
  865. <label for="tornpal-listing" style="display:block;">Listing Index (1 = lowest, 2 = 2nd lowest, etc)</label>
  866. <input id="tornpal-listing" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${bazaarListing || 1}">
  867. </div>
  868. <div style="margin-bottom:10px;">
  869. <label for="tornpal-margin-offset" style="display:block;">Margin (e.g., -1 for $1 less or 1% less)</label>
  870. <input id="tornpal-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${bazaarMarginOffset}">
  871. </div>
  872. <div style="margin-bottom:10px;">
  873. <label for="tornpal-margin-type" style="display:block;">Margin Type</label>
  874. <select id="tornpal-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
  875. <option value="absolute">Absolute ($)</option>
  876. <option value="percentage">Percentage (%)</option>
  877. </select>
  878. </div>
  879. <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
  880. <input id="tornpal-clamp" type="checkbox" style="margin-right:5px;" ${bazaarClamp ? "checked" : ""}>
  881. <label for="tornpal-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
  882. </div>
  883. </div>
  884. <hr style="border-top:1px solid #ccc; margin:8px 0;">
  885. <div style="text-align:right;">
  886. <button id="settings-save" style="margin-right:8px; padding:6px 10px; cursor:pointer;">Save</button>
  887. <button id="settings-cancel" style="padding:6px 10px; cursor:pointer;">Cancel</button>
  888. </div>
  889. </div>
  890. `);
  891. $overlay.append($modal);
  892. $("body").append($overlay);
  893. $("#pricing-source-select").val(pricingSource);
  894. $("#item-market-margin-type").val(itemMarketMarginType);
  895. $("#market-margin-type").val(marketMarginType);
  896. $("#tornpal-calc-method").val(bazaarCalcMethod);
  897. $("#tornpal-margin-type").val(bazaarMarginType);
  898. function toggleFields() {
  899. const src = $("#pricing-source-select").val();
  900. $("#market-value-options").toggle(src === "Market Value");
  901. $("#item-market-options").toggle(src === "Item Market");
  902. $("#tornpal-options").toggle(src === "Bazaars/TornPal");
  903. }
  904. $("#pricing-source-select").change(toggleFields);
  905. toggleFields();
  906. $("#settings-save").click(function () {
  907. var _a;
  908. const oldPricingSource = pricingSource;
  909. apiKey = ((_a = $("#api-key-input").val()) === null || _a === void 0 ? void 0 : _a.trim()) || "";
  910. pricingSource = $("#pricing-source-select").val();
  911. if (oldPricingSource !== pricingSource) {
  912. itemMarketCache = {};
  913. tornPalCache = { time: 0, data: null };
  914. }
  915. if (pricingSource === "Bazaars/TornPal") {
  916. bazaarCalcMethod = $("#tornpal-calc-method").val();
  917. bazaarMarginOffset = Number($("#tornpal-margin-offset").val() || 0);
  918. bazaarMarginType = $("#tornpal-margin-type").val();
  919. bazaarClamp = $("#tornpal-clamp").is(":checked");
  920. bazaarListing = Number($("#tornpal-listing").val() || 1);
  921. console.log("Saving Bazaar Settings:", {
  922. bazaarCalcMethod,
  923. bazaarMarginOffset,
  924. bazaarMarginType,
  925. bazaarClamp,
  926. bazaarListing
  927. });
  928. GM_setValue("bazaarCalcMethod", bazaarCalcMethod);
  929. GM_setValue("bazaarMarginOffset", bazaarMarginOffset);
  930. GM_setValue("bazaarMarginType", bazaarMarginType);
  931. GM_setValue("bazaarClamp", bazaarClamp);
  932. GM_setValue("bazaarListing", bazaarListing);
  933. }
  934. if (pricingSource === "Market Value") {
  935. marketMarginOffset = Number($("#market-margin-offset").val() || 0);
  936. marketMarginType = $("#market-margin-type").val();
  937. GM_setValue("marketMarginOffset", marketMarginOffset);
  938. GM_setValue("marketMarginType", marketMarginType);
  939. }
  940. if (pricingSource === "Item Market") {
  941. itemMarketListing = Number($("#item-market-listing").val() || 1);
  942. itemMarketOffset = Number($("#item-market-offset").val() || -1);
  943. itemMarketMarginType = $("#item-market-margin-type").val();
  944. itemMarketClamp = $("#item-market-clamp").is(":checked");
  945. GM_setValue("itemMarketListing", itemMarketListing);
  946. GM_setValue("itemMarketOffset", itemMarketOffset);
  947. GM_setValue("itemMarketMarginType", itemMarketMarginType);
  948. GM_setValue("itemMarketClamp", itemMarketClamp);
  949. }
  950. GM_setValue("tornApiKey", apiKey);
  951. GM_setValue("pricingSource", pricingSource);
  952. $overlay.remove();
  953. });
  954. $("#settings-cancel").click(() => $overlay.remove());
  955.  
  956. // Add refresh market values functionality
  957. $("#refresh-market-values").click(async function() {
  958. const $button = $(this);
  959. const $svg = $button.find("svg");
  960. const currentApiKey = $("#api-key-input").val().trim() || apiKey;
  961.  
  962. if (!currentApiKey) {
  963. alert("Please enter a valid API key first.");
  964. return;
  965. }
  966.  
  967. // Disable button and show loading state
  968. $button.prop("disabled", true);
  969. $svg.css("animation", "spin 1s linear infinite");
  970.  
  971. try {
  972. const response = await fetch(`https://api.torn.com/torn/?key=${currentApiKey}&selections=items&comment=wBazaarFiller`);
  973. const data = await response.json();
  974.  
  975. if (!data.items) {
  976. throw new Error(data.error?.error || "Failed to fetch market values");
  977. }
  978.  
  979. const filtered = {};
  980. for (const [id, item] of Object.entries(data.items)) {
  981. if (item.tradeable) {
  982. filtered[id] = {
  983. name: item.name,
  984. market_value: item.market_value,
  985. };
  986. }
  987. }
  988.  
  989. localStorage.setItem("tornItems", JSON.stringify(filtered));
  990. GM_setValue("lastUpdatedTime", Date.now());
  991.  
  992. // Show success message
  993. const $successMsg = $('<div style="color: #28a745; margin-top: 5px;">Market values refreshed successfully!</div>');
  994. $button.after($successMsg);
  995. setTimeout(() => $successMsg.remove(), 3000);
  996. } catch (error) {
  997. // Show error message
  998. const $errorMsg = $('<div style="color: #dc3545; margin-top: 5px;">Error: ' + error.message + '</div>');
  999. $button.after($errorMsg);
  1000. setTimeout(() => $errorMsg.remove(), 3000);
  1001. } finally {
  1002. // Re-enable button and remove loading state
  1003. $button.prop("disabled", false);
  1004. $svg.css("animation", "");
  1005. }
  1006. });
  1007. }
  1008. function addPricingSourceLink() {
  1009. if (document.getElementById("pricing-source-button"))
  1010. return;
  1011.  
  1012. const linksContainer = document.querySelector(".linksContainer___LiOTN");
  1013. if (!linksContainer) {
  1014. // Retry after a short delay if container not found
  1015. setTimeout(addPricingSourceLink, 100);
  1016. return;
  1017. }
  1018.  
  1019. try {
  1020. const link = document.createElement("a");
  1021. link.id = "pricing-source-button";
  1022. link.href = "#";
  1023. link.className = "linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9";
  1024. link.target = "_self";
  1025. link.rel = "noreferrer";
  1026.  
  1027. const iconSpan = document.createElement("span");
  1028. iconSpan.className = "iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV";
  1029. iconSpan.innerHTML = `
  1030. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
  1031. <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"/>
  1032. <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 0-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 0-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 0 .52 1.255l-.16.292c-.892 1.64.901 3.433 2.54 2.54l.292-.16a.873.873 0 0 0 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 0 1.255-.52l.292.16c1.64.893 3.433-.902 2.54-2.541l-.16-.292a.873.873 0 0 0-.52-1.255l-.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 0-.52-1.255l-.16-.292c-.893-1.64-.902-3.433-2.54-2.54l-.292.16a.873.873 0 0 0-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-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.115l.094-.318z"/>
  1033. </svg>
  1034. `;
  1035. link.appendChild(iconSpan);
  1036.  
  1037. const textSpan = document.createElement("span");
  1038. textSpan.className = "linkTitle____NPyM";
  1039. textSpan.textContent = "Bazaar Filler Settings";
  1040. link.appendChild(textSpan);
  1041.  
  1042. link.addEventListener("click", function (e) {
  1043. e.preventDefault();
  1044. openSettingsModal();
  1045. });
  1046.  
  1047. linksContainer.insertBefore(link, linksContainer.firstChild);
  1048. } catch (error) {
  1049. console.error("Error adding pricing source link:", error);
  1050. // Retry after a short delay if there was an error
  1051. setTimeout(addPricingSourceLink, 100);
  1052. }
  1053. }
  1054. function addBlackFridayToggle() {
  1055. if (document.getElementById("black-friday-toggle"))
  1056. return;
  1057.  
  1058. const linksContainer = document.querySelector(".linksContainer___LiOTN");
  1059. if (!linksContainer) {
  1060. // Retry after a short delay if container not found
  1061. setTimeout(addBlackFridayToggle, 100);
  1062. return;
  1063. }
  1064.  
  1065. try {
  1066. const link = document.createElement("a");
  1067. link.id = "black-friday-toggle";
  1068. link.href = "#";
  1069. link.className = "linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9";
  1070. if (blackFridayMode) {
  1071. link.classList.add("black-friday-active");
  1072. }
  1073. link.target = "_self";
  1074. link.rel = "noreferrer";
  1075.  
  1076. const iconSpan = document.createElement("span");
  1077. iconSpan.className = "iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV";
  1078. iconSpan.innerHTML = `
  1079. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="black-friday-icon" style="color: ${blackFridayMode ? "#28a745" : "inherit"}; fill: ${blackFridayMode ? "#28a745" : "currentColor"};">
  1080. <path d="M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718H4zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.05zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73l.348.086z"/>
  1081. </svg>
  1082. `;
  1083. link.appendChild(iconSpan);
  1084.  
  1085. const textSpan = document.createElement("span");
  1086. textSpan.className = "linkTitle____NPyM";
  1087. textSpan.textContent = blackFridayMode ? "Black Friday: ON" : "Black Friday: OFF";
  1088. link.appendChild(textSpan);
  1089.  
  1090. link.addEventListener("click", function (e) {
  1091. e.preventDefault();
  1092. blackFridayMode = !blackFridayMode;
  1093. GM_setValue("blackFridayMode", blackFridayMode);
  1094. textSpan.textContent = blackFridayMode ? "Black Friday: ON" : "Black Friday: OFF";
  1095. const svg = this.querySelector(".black-friday-icon");
  1096. if (svg) {
  1097. svg.style.color = blackFridayMode ? "#28a745" : "inherit";
  1098. svg.style.fill = blackFridayMode ? "#28a745" : "currentColor";
  1099. }
  1100. if (blackFridayMode) {
  1101. link.classList.add("black-friday-active");
  1102. }
  1103. else {
  1104. link.classList.remove("black-friday-active");
  1105. }
  1106. });
  1107.  
  1108. const settingsButton = document.getElementById("pricing-source-button");
  1109. if (settingsButton) {
  1110. linksContainer.insertBefore(link, settingsButton);
  1111. }
  1112. else {
  1113. linksContainer.insertBefore(link, linksContainer.firstChild);
  1114. }
  1115. } catch (error) {
  1116. console.error("Error adding black friday toggle:", error);
  1117. // Retry after a short delay if there was an error
  1118. setTimeout(addBlackFridayToggle, 100);
  1119. }
  1120. }
  1121. function addAddPageCheckboxes() {
  1122. $(".items-cont .title-wrap").each(function () {
  1123. const $el = $(this);
  1124. if ($el.find(".checkbox-wrapper").length)
  1125. return;
  1126. $el.css("position", "relative");
  1127. const wrapper = $('<div class="checkbox-wrapper"></div>');
  1128. const checkbox = $("<input>", {
  1129. type: "checkbox",
  1130. class: "item-toggle",
  1131. click: async function (e) {
  1132. e.stopPropagation();
  1133. if (!GM_getValue("tornApiKey", "")) {
  1134. alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
  1135. $(this).prop("checked", false);
  1136. openSettingsModal();
  1137. return;
  1138. }
  1139. await updateAddRow($(this).closest("li.clearfix"), this.checked);
  1140. },
  1141. });
  1142. wrapper.append(checkbox);
  1143. $el.append(wrapper);
  1144. });
  1145. $(document)
  1146. .off("dblclick", ".amount input")
  1147. .on("dblclick", ".amount input", function () {
  1148. const $row = $(this).closest("li.clearfix");
  1149. const qty = $row.find(".item-amount.qty").text().trim();
  1150. if (qty) {
  1151. $(this).val(qty);
  1152. $(this)[0].dispatchEvent(new Event("input", { bubbles: true }));
  1153. $(this)[0].dispatchEvent(new Event("keyup", { bubbles: true }));
  1154. }
  1155. });
  1156.  
  1157. // Add "Select All" button if it doesn't exist yet
  1158. if ($(".select-all-action").length === 0) {
  1159. const $clearAllBtn = $(".clear-action");
  1160. if ($clearAllBtn.length) {
  1161. const $selectAllBtn = $('<span class="select-all-action t-blue h c-pointer" style="margin-left: 15px;">Select All</span>');
  1162. $clearAllBtn.before($selectAllBtn);
  1163.  
  1164. $selectAllBtn.on("click", async function(e) {
  1165. e.preventDefault();
  1166. if (!GM_getValue("tornApiKey", "")) {
  1167. alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
  1168. openSettingsModal();
  1169. return;
  1170. }
  1171.  
  1172. // Find the active category panel (the one currently displayed)
  1173. let $activePanel = $(".items-cont.ui-tabs-panel[style*='display: block']");
  1174.  
  1175. // If no panel is found with display:block, try finding the active tab and its corresponding panel
  1176. if (!$activePanel.length) {
  1177. const $activeTab = $(".ui-tabs-active.ui-state-active");
  1178. if ($activeTab.length) {
  1179. const tabId = $activeTab.find("a").attr("href").replace("#", "");
  1180. $activePanel = $(`.items-cont.ui-tabs-panel[data-reactid*='$${tabId}']`);
  1181. }
  1182.  
  1183. // If still no active panel, fall back to any visible panel
  1184. if (!$activePanel.length) {
  1185. $activePanel = $(".items-cont.ui-tabs-panel").filter(function() {
  1186. return $(this).css("display") !== "none";
  1187. });
  1188. }
  1189. }
  1190.  
  1191. if ($activePanel.length) {
  1192. // Find all unchecked checkboxes in that panel
  1193. const $checkboxes = $activePanel.find("li.clearfix:not(.disabled) .checkbox-wrapper input.item-toggle:not(:checked)");
  1194.  
  1195. if ($checkboxes.length === 0) {
  1196. return; // No checkboxes to check
  1197. }
  1198.  
  1199. // Check all boxes and trigger their click handlers
  1200. for (let i = 0; i < $checkboxes.length; i++) {
  1201. const $checkbox = $($checkboxes[i]);
  1202. $checkbox.prop("checked", true);
  1203. const $row = $checkbox.closest("li.clearfix");
  1204. await updateAddRow($row, true);
  1205. }
  1206. }
  1207. });
  1208. }
  1209. }
  1210. }
  1211. function addManagePageCheckboxes() {
  1212. $(".item___jLJcf").each(function () {
  1213. const $row = $(this);
  1214. const $desc = $row.find(".desc___VJSNQ");
  1215. if (!$desc.length || $desc.find(".checkbox-wrapper").length)
  1216. return;
  1217. $desc.css("position", "relative");
  1218. const wrapper = $('<div class="checkbox-wrapper"></div>');
  1219. const checkbox = $("<input>", {
  1220. type: "checkbox",
  1221. class: "item-toggle",
  1222. click: async function (e) {
  1223. e.stopPropagation();
  1224. if (!GM_getValue("tornApiKey", "")) {
  1225. alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
  1226. $(this).prop("checked", false);
  1227. openSettingsModal();
  1228. return;
  1229. }
  1230. const $row = $(this).closest(".item___jLJcf");
  1231. if ($row.length === 0) {
  1232. const $correctRow = $(this).closest(".item___jLJcf");
  1233. if ($correctRow.length > 0) {
  1234. if (window.innerWidth <= 784) {
  1235. const $manageBtn = $correctRow.find('button[aria-label="Manage"]').first();
  1236. if ($manageBtn.length) {
  1237. if (!$manageBtn.find("span").hasClass("active___OTFsm")) {
  1238. $manageBtn.click();
  1239. }
  1240. setTimeout(async () => {
  1241. await updateManageRowMobile($correctRow, this.checked);
  1242. }, 200);
  1243. return;
  1244. }
  1245. }
  1246. await updateManageRow($correctRow, this.checked);
  1247. return;
  1248. }
  1249. console.warn("Row not found with either selector");
  1250. return;
  1251. }
  1252. await updateManageRow($row, this.checked);
  1253. },
  1254. });
  1255. wrapper.append(checkbox);
  1256. $desc.append(wrapper);
  1257. });
  1258. }
  1259.  
  1260. const storedItems = localStorage.getItem("tornItems");
  1261. const lastUpdatedTime = GM_getValue("lastUpdatedTime", 0);
  1262. const now = Date.now();
  1263. const oneDayMs = 24 * 60 * 60 * 1000;
  1264. const lastUpdatedDate = new Date(lastUpdatedTime);
  1265. const todayUTC = new Date().toISOString().split("T")[0];
  1266. const lastUpdatedUTC = lastUpdatedDate.toISOString().split("T")[0];
  1267. if (apiKey &&
  1268. (!storedItems || lastUpdatedUTC < todayUTC || now - lastUpdatedTime >= oneDayMs)) {
  1269. fetch(`https://api.torn.com/torn/?key=${apiKey}&selections=items&comment=wBazaarFiller`)
  1270. .then((r) => r.json())
  1271. .then((data) => {
  1272. if (!data.items) {
  1273. console.error("Failed to fetch Torn items or no items found. Possibly invalid API key or rate limit.");
  1274. return;
  1275. }
  1276. const filtered = {};
  1277. for (const [id, item] of Object.entries(data.items)) {
  1278. if (item.tradeable) {
  1279. filtered[id] = {
  1280. name: item.name,
  1281. market_value: item.market_value,
  1282. };
  1283. }
  1284. }
  1285. localStorage.setItem("tornItems", JSON.stringify(filtered));
  1286. GM_setValue("lastUpdatedTime", now);
  1287. })
  1288. .catch((err) => {
  1289. console.error("Error fetching Torn items:", err);
  1290. });
  1291. }
  1292. const domObserver = new MutationObserver(() => {
  1293. try {
  1294. if (window.location.hash === "#/add") {
  1295. addAddPageCheckboxes();
  1296. }
  1297. else if (window.location.hash === "#/manage") {
  1298. addManagePageCheckboxes();
  1299. }
  1300. addPricingSourceLink();
  1301. addBlackFridayToggle();
  1302. attachPriceFieldObservers();
  1303. } catch (error) {
  1304. console.error("Error in DOM observer:", error);
  1305. // Retry after a short delay
  1306. setTimeout(() => {
  1307. if (window.location.hash === "#/add") {
  1308. addAddPageCheckboxes();
  1309. }
  1310. else if (window.location.hash === "#/manage") {
  1311. addManagePageCheckboxes();
  1312. }
  1313. addPricingSourceLink();
  1314. addBlackFridayToggle();
  1315. attachPriceFieldObservers();
  1316. }, 1000);
  1317. }
  1318. });
  1319.  
  1320. // Observe the entire document for changes
  1321. domObserver.observe(document.body, {
  1322. childList: true,
  1323. subtree: true,
  1324. attributes: true,
  1325. characterData: true
  1326. });
  1327.  
  1328. // Initialize on page load
  1329. window.addEventListener('load', () => {
  1330. setTimeout(() => {
  1331. if (window.location.hash === "#/add") {
  1332. addAddPageCheckboxes();
  1333. }
  1334. else if (window.location.hash === "#/manage") {
  1335. addManagePageCheckboxes();
  1336. }
  1337. addPricingSourceLink();
  1338. addBlackFridayToggle();
  1339. attachPriceFieldObservers();
  1340. }, 100);
  1341. });
  1342.  
  1343. // Initialize on hash change
  1344. window.addEventListener("hashchange", () => {
  1345. currentPage = window.location.hash;
  1346. setTimeout(() => {
  1347. if (currentPage === "#/add") {
  1348. addAddPageCheckboxes();
  1349. }
  1350. else if (currentPage === "#/manage") {
  1351. addManagePageCheckboxes();
  1352. }
  1353. addPricingSourceLink();
  1354. addBlackFridayToggle();
  1355. attachPriceFieldObservers();
  1356. }, 100);
  1357. });
  1358.  
  1359. // Add a periodic check to ensure UI elements are present
  1360. setInterval(() => {
  1361. const shouldHaveUI = window.location.hash === "#/add" || window.location.hash === "#/manage";
  1362. const hasUI = $(".checkbox-wrapper").length > 0 || $("#pricing-source-button").length > 0;
  1363.  
  1364. if (shouldHaveUI && !hasUI) {
  1365. console.log("UI elements missing, reinitializing...");
  1366. if (window.location.hash === "#/add") {
  1367. addAddPageCheckboxes();
  1368. }
  1369. else if (window.location.hash === "#/manage") {
  1370. addManagePageCheckboxes();
  1371. }
  1372. addPricingSourceLink();
  1373. addBlackFridayToggle();
  1374. attachPriceFieldObservers();
  1375. }
  1376. }, 5000);
  1377.  
  1378. $(document).on("click", "button.undo___FTgvP", function (e) {
  1379. e.preventDefault();
  1380. $(".item___jLJcf .checkbox-wrapper input.item-toggle:checked").each(function () {
  1381. $(this).prop("checked", false);
  1382. const $row = $(this).closest(".item___jLJcf");
  1383. updateManageRow($row, false);
  1384. });
  1385. });
  1386. $(document).on("click", ".clear-action", function (e) {
  1387. e.preventDefault();
  1388. $("li.clearfix .checkbox-wrapper input.item-toggle:checked").each(function () {
  1389. $(this).prop("checked", false);
  1390. const $row = $(this).closest("li.clearfix");
  1391. updateAddRow($row, false);
  1392. });
  1393. });
  1394. $(document).ready(function () {
  1395. itemMarketCache = {};
  1396. tornPalCache = { time: 0, data: null };
  1397. });
  1398. })();