Torn Item Market Highlighter

Highlight items in the item market/bazaars that are at or below Arson Warehouse Pricelist and Market Value

目前为 2024-10-26 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Torn Item Market Highlighter
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.16
  5. // @description Highlight items in the item market/bazaars that are at or below Arson Warehouse Pricelist and Market Value
  6. // @author You
  7. // @match https://www.torn.com/page.php?sid=ItemMarket*
  8. // @match https://www.torn.com/bazaar.php*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_addStyle
  13. // @grant GM.xmlHttpRequest
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. GM_addStyle(`
  20. /* Existing styles */
  21. .price-indicators-row {
  22. display: inline-flex;
  23. gap: 4px;
  24. margin-left: 4px;
  25. font-size: 10px;
  26. vertical-align: middle;
  27. }
  28.  
  29. .price-indicator {
  30. padding: 1px 3px;
  31. border-radius: 3px;
  32. font-weight: bold;
  33. white-space: nowrap;
  34. display: inline-flex;
  35. align-items: center;
  36. justify-content: center;
  37. gap: 2px;
  38. min-width: 44px;
  39. max-width: fit-content;
  40. text-align: center;
  41. }
  42.  
  43. .diff-90-100 {
  44. background: #004d00;
  45. color: white;
  46. }
  47. .diff-60-90 {
  48. background: #006700;
  49. color: white;
  50. }
  51. .diff-30-60 {
  52. background: #008100;
  53. color: white;
  54. }
  55. .diff-0-30 {
  56. background: #009b00;
  57. color: white;
  58. }
  59. .diff0-30 {
  60. background: #cc0000;
  61. color: white;
  62. width: fit-content;
  63. padding: 1px 4px;
  64. }
  65. .diff30-60 {
  66. background: #b30000;
  67. color: white;
  68. width: fit-content;
  69. padding: 1px 4px;
  70. }
  71. .diff60-90 {
  72. background: #990000;
  73. color: white;
  74. width: fit-content;
  75. padding: 1px 4px;
  76. }
  77. .diff90-plus {
  78. background: #800000;
  79. color: white;
  80. width: fit-content;
  81. padding: 1px 4px;
  82. }
  83. .diff-equal {
  84. background: #666666;
  85. color: white;
  86. width: fit-content;
  87. padding: 1px 4px;
  88. }
  89.  
  90. .icon-exchange {
  91. display: inline-block;
  92. width: 12px;
  93. height: 12px;
  94. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='white' d='M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z'/%3E%3C/svg%3E");
  95. }
  96.  
  97. .icon-store {
  98. display: inline-block;
  99. width: 12px;
  100. height: 12px;
  101. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 616 512'%3E%3Cpath fill='white' d='M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-33.5 53.5-3.8 127.9 58.8 136.4 4.5.6 9.1.9 13.7.9 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18.1 20.1 44.3 33.1 73.8 33.1 4.7 0 9.2-.3 13.7-.9 62.8-8.4 92.6-82.8 59-136.4zM529.5 288c-10 0-19.9-1.5-29.5-3.8V384H116v-99.8c-9.6 2.2-19.5 3.8-29.5 3.8-6 0-12.1-.4-18-1.2-5.6-.8-11.1-2.1-16.4-3.6V480c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32V283.2c-5.4 1.6-10.8 2.9-16.4 3.6-6.1.8-12.1 1.2-18.2 1.2z'/%3E%3C/svg%3E");
  102. }
  103.  
  104. .icon-exchange, .icon-store {
  105. display: inline-block;
  106. width: 10px;
  107. height: 10px;
  108. background-size: contain;
  109. background-repeat: no-repeat;
  110. background-position: center;
  111. vertical-align: middle;
  112. margin-right: 2px;
  113. }
  114.  
  115. /* Desktop layout improvements */
  116. @media (min-width: 785px) {
  117. .sellerRow___AI0m6 {
  118. padding: 4px 4px !important;
  119. display: flex !important;
  120. align-items: center !important;
  121. gap: 2px !important;
  122. width: 100% !important;
  123. }
  124.  
  125. .thumbnail___M_h9v {
  126. flex-shrink: 0;
  127. width: 40px !important;
  128. margin-right: 4px !important;
  129. }
  130.  
  131. .userInfoWrapper___B2a2P {
  132. flex-shrink: 0;
  133. min-width: 110px;
  134. margin-right: 4px !important;
  135. }
  136.  
  137. /* Stack indicators only in seller rows */
  138. .sellerRow___AI0m6 .price-indicators-row {
  139. display: inline-flex !important;
  140. flex-direction: column !important;
  141. gap: 2px !important;
  142. margin-left: 2px !important;
  143. margin-right: 0 !important;
  144. }
  145.  
  146. .price___Uwiv2 {
  147. display: flex !important;
  148. align-items: center !important;
  149. flex-shrink: 0;
  150. min-width: 85px;
  151. margin-right: 0 !important;
  152. }
  153.  
  154. .available___xegv_ {
  155. flex-shrink: 0;
  156. min-width: 55px;
  157. text-align: right;
  158. margin-right: 2px !important;
  159. }
  160.  
  161. .buyControlsInRow___GVAKp {
  162. flex-shrink: 0;
  163. }
  164.  
  165. .buyControls___MxiIN {
  166. display: flex !important;
  167. align-items: center !important;
  168. gap: 2px !important;
  169. }
  170.  
  171. .amountInputWrapper___a4BMt {
  172. min-width: 55px !important;
  173. width: 55px !important;
  174. flex-shrink: 0;
  175. }
  176.  
  177. .input-money {
  178. min-width: 45px !important;
  179. width: 100% !important;
  180. padding: 0 2px !important;
  181. }
  182.  
  183. .buyButton___Flkhg {
  184. flex-shrink: 0;
  185. min-width: 65px;
  186. padding-left: 8px !important;
  187. padding-right: 8px !important;
  188. }
  189.  
  190. .price-indicator {
  191. padding: 1px 4px !important;
  192. min-width: 0 !important;
  193. }
  194.  
  195. .space___qCLQp {
  196. display: none !important;
  197. }
  198. }
  199.  
  200. /* Mobile-specific styles */
  201. @media (max-width: 784px) {
  202. .sellerRow___Ca2pK {
  203. display: grid !important;
  204. grid-template-columns: minmax(80px, 1fr) auto auto auto !important;
  205. align-items: center !important;
  206. gap: 8px !important;
  207. padding: 8px 12px !important;
  208. }
  209.  
  210. .sellerRow___Ca2pK:first-child {
  211. font-weight: bold;
  212. background-color: rgba(0, 0, 0, 0.1);
  213. }
  214.  
  215. .userInfoWrapper___B2a2P {
  216. min-width: 80px;
  217. max-width: 120px;
  218. }
  219.  
  220. .price___v8rRx {
  221. position: relative;
  222. display: flex;
  223. flex-direction: column;
  224. align-items: center;
  225. gap: 2px;
  226. min-width: 85px;
  227. }
  228.  
  229. .price-indicators-row {
  230. position: static !important;
  231. display: flex !important;
  232. flex-direction: column !important;
  233. gap: 2px !important;
  234. margin-top: 2px !important;
  235. font-size: 9px !important;
  236. align-items: center !important;
  237. }
  238.  
  239. .price-indicator {
  240. padding: 1px 4px !important;
  241. white-space: nowrap !important;
  242. text-align: center !important;
  243. justify-content: center !important;
  244. width: fit-content !important;
  245. min-width: 0 !important;
  246. margin: 0 auto !important;
  247. display: inline-flex !important;
  248. align-items: center !important;
  249. }
  250.  
  251. .available___jtANf {
  252. text-align: center;
  253. min-width: 30px;
  254. }
  255.  
  256. .showBuyControlsButton___K8f72 {
  257. padding: 6px !important;
  258. display: flex !important;
  259. align-items: center !important;
  260. justify-content: center !important;
  261. }
  262.  
  263. .userInfoHead___LXxjB,
  264. .priceHead___Yo8ku,
  265. .availableHead___BkcpB,
  266. .showBuyControlsHead___SczEn {
  267. text-align: center !important;
  268. }
  269.  
  270. .icon-exchange,
  271. .icon-store {
  272. width: 8px !important;
  273. height: 8px !important;
  274. margin: 0 2px 0 0 !important;
  275. display: inline-flex !important;
  276. align-items: center !important;
  277. justify-content: center !important;
  278. }
  279. }
  280.  
  281. /* Styles for the floating container */
  282. #floating-container {
  283. position: fixed;
  284. top: 100px;
  285. left: -200px; /* Adjust this value to the negative width of the container */
  286. z-index: 1000;
  287. background-color: rgba(0, 0, 0, 0.7);
  288. padding: 10px;
  289. border-top-right-radius: 10px;
  290. border-bottom-right-radius: 10px;
  291. color: white;
  292. transition: left 0.3s ease;
  293. }
  294.  
  295. #floating-container.expanded {
  296. left: 0;
  297. }
  298.  
  299. #floating-container button {
  300. display: block;
  301. width: 100%;
  302. margin-bottom: 5px;
  303. background-color: #333;
  304. color: white;
  305. border: none;
  306. padding: 8px;
  307. border-radius: 5px;
  308. cursor: pointer;
  309. }
  310.  
  311. #floating-container button:hover {
  312. background-color: #555;
  313. }
  314.  
  315. /* Styles for the toggle button */
  316. #toggle-button {
  317. position: fixed;
  318. top: 100px;
  319. left: 0;
  320. background-color: rgba(0,0,0,0.7);
  321. border-top-right-radius: 5px;
  322. border-bottom-right-radius: 5px;
  323. width: 25px;
  324. height: 50px;
  325. cursor: pointer;
  326. display: flex;
  327. align-items: center;
  328. justify-content: center;
  329. color: white;
  330. font-size: 18px;
  331. z-index: 1001;
  332. }
  333.  
  334. .toast {
  335. position: fixed;
  336. top: 10px;
  337. left: 50%;
  338. transform: translateX(-50%);
  339. background-color: rgba(0,0,0,0.7);
  340. color: white;
  341. padding: 10px;
  342. border-radius: 5px;
  343. z-index: 9999; /* Ensure it's above other elements */
  344. opacity: 0;
  345. transition: opacity 0.5s ease;
  346. }
  347.  
  348. .toast.show {
  349. opacity: 1;
  350. }
  351.  
  352. `);
  353.  
  354. let item_prices = {};
  355. let torn_market_values = {};
  356.  
  357. try {
  358. item_prices = JSON.parse(GM_getValue("AWH_Prices", "{}"));
  359. torn_market_values = JSON.parse(GM_getValue("Torn_Market_Values", "{}"));
  360. } catch (e) {}
  361.  
  362. function getTornIDFromPage() {
  363. const tornUserInput = document.getElementById('torn-user');
  364. if (tornUserInput) {
  365. try {
  366. const userData = JSON.parse(tornUserInput.value);
  367. return userData.id;
  368. } catch (e) {
  369. console.error('Error parsing torn-user data:', e);
  370. return null;
  371. }
  372. }
  373. return null;
  374. }
  375.  
  376. function createFloatingContainer() {
  377. const container = document.createElement('div');
  378. container.id = 'floating-container';
  379. // Start collapsed by default (left position is negative in CSS)
  380.  
  381. const toggleButton = document.createElement('div');
  382. toggleButton.id = 'toggle-button';
  383. toggleButton.innerHTML = '☰'; // Hamburger icon
  384.  
  385. toggleButton.addEventListener('click', () => {
  386. if (container.classList.contains('expanded')) {
  387. container.classList.remove('expanded');
  388. } else {
  389. container.classList.add('expanded');
  390. }
  391. });
  392.  
  393. const buttonsWrapper = document.createElement('div');
  394.  
  395. const addAWHButton = document.createElement('button');
  396. addAWHButton.textContent = GM_getValue("AWH_Key", "") ? 'Edit AWH API key' : 'Add AWH API key';
  397. addAWHButton.addEventListener('click', () => {
  398. let AWH_Key = GM_getValue("AWH_Key", "");
  399. AWH_Key = prompt("Enter your AWH API key", AWH_Key);
  400. if (AWH_Key !== null) { // Only proceed if user didn't press Cancel
  401. if (AWH_Key.trim() === "") {
  402. // If field was cleared, remove the key and clear AWH prices
  403. GM_setValue("AWH_Key", "");
  404. GM_setValue("AWH_Prices", "{}");
  405. item_prices = {}; // Clear the current prices in memory
  406. showToast("AWH API key and prices removed successfully!", 'success');
  407. addAWHButton.textContent = 'Add AWH API key';
  408. } else {
  409. // If new key provided, save it
  410. GM_setValue("AWH_Key", AWH_Key);
  411. showToast("AWH API key saved successfully!", 'success');
  412. addAWHButton.textContent = 'Edit AWH API key';
  413. checkAndUpdatePrices();
  414. }
  415. updateButtonsVisibility();
  416. processElements(); // Refresh the display
  417. }
  418. });
  419.  
  420. const addTornButton = document.createElement('button');
  421. addTornButton.textContent = GM_getValue("Torn_API_Key", "") ? 'Edit Torn API key' : 'Add Torn API key';
  422. addTornButton.addEventListener('click', () => {
  423. let tornApiKey = GM_getValue("Torn_API_Key", "");
  424. tornApiKey = prompt("Enter your Torn API key", tornApiKey);
  425. if (tornApiKey !== null) { // Only proceed if user didn't press Cancel
  426. if (tornApiKey.trim() === "") {
  427. // If field was cleared, remove the key and clear market values
  428. GM_setValue("Torn_API_Key", "");
  429. GM_setValue("Torn_Market_Values", "{}");
  430. torn_market_values = {}; // Clear the current values in memory
  431. showToast("Torn API key and market values removed successfully!", 'success');
  432. addTornButton.textContent = 'Add Torn API key';
  433. } else {
  434. // If new key provided, save it
  435. GM_setValue("Torn_API_Key", tornApiKey);
  436. showToast("Torn API key saved successfully!", 'success');
  437. addTornButton.textContent = 'Edit Torn API key';
  438. getTornMarketValues();
  439. }
  440. updateButtonsVisibility();
  441. processElements(); // Refresh the display
  442. }
  443. });
  444.  
  445.  
  446. const getAWHPricesButton = document.createElement('button');
  447. getAWHPricesButton.textContent = 'Get AWH Prices Now';
  448. getAWHPricesButton.addEventListener('click', getAWHPrices);
  449.  
  450. const getMarketValuesButton = document.createElement('button');
  451. getMarketValuesButton.textContent = 'Get Market Values Now';
  452. getMarketValuesButton.addEventListener('click', getTornMarketValues);
  453.  
  454. buttonsWrapper.appendChild(addAWHButton);
  455. buttonsWrapper.appendChild(addTornButton);
  456. buttonsWrapper.appendChild(getAWHPricesButton);
  457. buttonsWrapper.appendChild(getMarketValuesButton);
  458.  
  459. container.appendChild(buttonsWrapper);
  460.  
  461. document.body.appendChild(container);
  462. document.body.appendChild(toggleButton);
  463.  
  464. // Function to update button visibility based on API keys
  465. function updateButtonsVisibility() {
  466. const AWH_Key = GM_getValue("AWH_Key", "");
  467. const tornApiKey = GM_getValue("Torn_API_Key", "");
  468.  
  469. addAWHButton.textContent = AWH_Key ? 'Edit AWH API key' : 'Add AWH API key';
  470. addTornButton.textContent = tornApiKey ? 'Edit Torn API key' : 'Add Torn API key';
  471.  
  472. if (!AWH_Key) {
  473. getAWHPricesButton.style.display = 'none';
  474. // Ensure AWH prices are cleared if key is removed
  475. if (Object.keys(item_prices).length > 0) {
  476. item_prices = {};
  477. GM_setValue("AWH_Prices", "{}");
  478. }
  479. } else {
  480. getAWHPricesButton.style.display = '';
  481. }
  482.  
  483. if (!tornApiKey) {
  484. getMarketValuesButton.style.display = 'none';
  485. // Ensure market values are cleared if key is removed
  486. if (Object.keys(torn_market_values).length > 0) {
  487. torn_market_values = {};
  488. GM_setValue("Torn_Market_Values", "{}");
  489. }
  490. } else {
  491. getMarketValuesButton.style.display = '';
  492. }
  493. }
  494.  
  495.  
  496. updateButtonsVisibility(); // Set initial visibility
  497. }
  498.  
  499.  
  500.  
  501. function showToast(message, type = 'info') {
  502. const toast = document.createElement('div');
  503. toast.className = 'toast';
  504. toast.textContent = message;
  505.  
  506. if (type === 'success') {
  507. toast.style.backgroundColor = 'green';
  508. } else if (type === 'error') {
  509. toast.style.backgroundColor = 'red';
  510. } else {
  511. toast.style.backgroundColor = 'rgba(0,0,0,0.7)';
  512. }
  513.  
  514. document.body.appendChild(toast);
  515. setTimeout(() => {
  516. toast.classList.add('show');
  517. }, 100);
  518.  
  519. setTimeout(() => {
  520. toast.classList.remove('show');
  521. setTimeout(() => {
  522. toast.remove();
  523. }, 500);
  524. }, 3000); // Show for 3 seconds
  525. }
  526.  
  527.  
  528. function checkAndUpdatePrices() {
  529. const stored_torn_id = GM_getValue("AWH_TornID", "");
  530. const page_torn_id = getTornIDFromPage();
  531. const AWH_Key = GM_getValue("AWH_Key", "");
  532.  
  533. // Update stored Torn ID if we found one on the page
  534. if (page_torn_id && page_torn_id !== stored_torn_id) {
  535. GM_setValue("AWH_TornID", page_torn_id);
  536. }
  537.  
  538. // Use page Torn ID if available, fall back to stored ID
  539. const torn_id = page_torn_id || stored_torn_id;
  540.  
  541. if (AWH_Key) {
  542. getAWHPrices();
  543. }
  544. }
  545.  
  546. function scheduleNextUpdate() {
  547. const now = new Date();
  548. const target = new Date(now);
  549. target.setUTCHours(20, 15, 0, 0); // 8:15 PM UTC
  550.  
  551. if (now > target) {
  552. target.setDate(target.getDate() + 1);
  553. }
  554.  
  555. const msUntilUpdate = target - now;
  556. setTimeout(() => {
  557. getAWHPrices();
  558. getTornMarketValues();
  559. scheduleNextUpdate();
  560. }, msUntilUpdate);
  561. }
  562.  
  563. function getTornMarketValues() {
  564. const tornApiKey = GM_getValue("Torn_API_Key", "");
  565.  
  566. if (!tornApiKey) {
  567. // Torn API key not set, skipping
  568. return;
  569. }
  570.  
  571. GM.xmlHttpRequest({
  572. method: "GET",
  573. url: `https://api.torn.com/torn/?key=${tornApiKey}&selections=items`,
  574. onload: function(response) {
  575. try {
  576. const data = JSON.parse(response.responseText);
  577. if (data.items) {
  578. Object.entries(data.items).forEach(([itemId, item]) => {
  579. torn_market_values[itemId] = item.market_value || 0;
  580. });
  581. GM_setValue("Torn_Market_Values", JSON.stringify(torn_market_values));
  582. GM_setValue("lastMarketUpdate", Date.now());
  583. showToast('Market values updated successfully!', 'success');
  584. processElements();
  585. } else {
  586. showToast('No market value data received. Please check your API key.', 'error');
  587. }
  588. } catch (e) {
  589. showToast('Error updating market values. Please check your API key.', 'error');
  590. }
  591. },
  592. onerror: function() {
  593. showToast('Failed to connect to Torn API. Please try again later.', 'error');
  594. }
  595. });
  596. }
  597.  
  598.  
  599. function getAWHPrices() {
  600. const AWH_Key = GM_getValue("AWH_Key", "");
  601. const torn_id = getTornIDFromPage() || GM_getValue("AWH_TornID", "");
  602.  
  603. if (!AWH_Key) {
  604. // AWH API key not set, skipping
  605. return;
  606. }
  607.  
  608. item_prices = {};
  609. GM.xmlHttpRequest({
  610. method: "GET",
  611. url: `https://arsonwarehouse.com/api/v1/bids/${torn_id}`,
  612. headers: {
  613. "Authorization": "Basic " + btoa(AWH_Key + ':')
  614. },
  615. onload: function(response) {
  616. try {
  617. const items = JSON.parse(response.responseText);
  618. if (items.bids?.length > 0) {
  619. items.bids.forEach(bid => {
  620. if (bid.item_id && bid.bids?.length > 0) {
  621. item_prices[bid.item_id] = bid.bids[0].price || 0;
  622. }
  623. });
  624. GM_setValue("AWH_Prices", JSON.stringify(item_prices));
  625. GM_setValue("lastUpdate", Date.now());
  626. showToast('Prices updated successfully!', 'success');
  627. processElements();
  628. } else {
  629. showToast('No price data received. Please check your credentials.', 'error');
  630. }
  631. } catch (e) {
  632. showToast('Error updating prices. Please check your credentials.', 'error');
  633. }
  634. },
  635. onerror: function() {
  636. showToast('Failed to connect to AWH. Please try again later.', 'error');
  637. }
  638. });
  639. }
  640.  
  641.  
  642. function addPriceIndicator(itemId, itemPrice, container) {
  643. // Remove any existing indicators row
  644. const existingRow = container.nextElementSibling;
  645. if (existingRow?.classList.contains('price-indicators-row')) {
  646. existingRow.remove();
  647. }
  648.  
  649. // Create new indicators row
  650. const indicatorsRow = document.createElement('div');
  651. indicatorsRow.classList.add('price-indicators-row');
  652.  
  653. // Get quantity if we're in a seller row
  654. let quantity = 1;
  655. if (container.closest('.sellerRow___AI0m6')) {
  656. const quantityElement = container.closest('.sellerRow___AI0m6').querySelector('.available___xegv_');
  657. if (quantityElement) {
  658. // Extract number from "X available" text
  659. const match = quantityElement.textContent.match(/(\d+)\s+available/);
  660. quantity = match ? parseInt(match[1]) : 1;
  661. }
  662. }
  663.  
  664. // AWH Price comparison (using exchange icon)
  665. if (item_prices[itemId]) {
  666. const awhPrice = item_prices[itemId];
  667. const awhPriceDiff = Math.round(((awhPrice - itemPrice) / awhPrice) * 100 * 100) / 100;
  668. const potentialProfit = (awhPrice - itemPrice) * quantity;
  669.  
  670. const awhIndicator = document.createElement('span');
  671. awhIndicator.classList.add('price-indicator');
  672. awhIndicator.title = `Potential profit: $${potentialProfit.toLocaleString()}` +
  673. (quantity > 1 ? ` (${quantity}x)` : '');
  674. const icon = document.createElement('span');
  675. icon.classList.add('icon-exchange');
  676. awhIndicator.appendChild(icon);
  677. awhIndicator.appendChild(document.createTextNode(
  678. ` ${awhPriceDiff > 0 ? '-' : '+'}${Math.abs(Math.round(awhPriceDiff))}%`
  679. ));
  680.  
  681. if (Math.abs(awhPriceDiff) < 0.5) {
  682. awhIndicator.classList.add('diff-equal');
  683. } else if (awhPriceDiff > 0) {
  684. if (awhPriceDiff >= 90) awhIndicator.classList.add('diff-90-100');
  685. else if (awhPriceDiff >= 60) awhIndicator.classList.add('diff-60-90');
  686. else if (awhPriceDiff >= 30) awhIndicator.classList.add('diff-30-60');
  687. else awhIndicator.classList.add('diff-0-30');
  688. } else {
  689. if (awhPriceDiff <= -90) awhIndicator.classList.add('diff90-plus');
  690. else if (awhPriceDiff <= -60) awhIndicator.classList.add('diff60-90');
  691. else if (awhPriceDiff <= -30) awhIndicator.classList.add('diff30-60');
  692. else awhIndicator.classList.add('diff0-30');
  693. }
  694.  
  695. indicatorsRow.appendChild(awhIndicator);
  696. }
  697.  
  698. // Market Value comparison (using store icon)
  699. if (torn_market_values[itemId]) {
  700. const marketValue = torn_market_values[itemId];
  701. const marketPriceDiff = Math.round(((marketValue - itemPrice) / marketValue) * 100 * 100) / 100;
  702. const potentialProfit = (marketValue - itemPrice) * quantity;
  703.  
  704. const marketIndicator = document.createElement('span');
  705. marketIndicator.classList.add('price-indicator');
  706. marketIndicator.title = `Potential profit: $${potentialProfit.toLocaleString()}` +
  707. (quantity > 1 ? ` (${quantity}x)` : '');
  708. const icon = document.createElement('span');
  709. icon.classList.add('icon-store');
  710. marketIndicator.appendChild(icon);
  711. marketIndicator.appendChild(document.createTextNode(
  712. ` ${marketPriceDiff > 0 ? '-' : '+'}${Math.abs(Math.round(marketPriceDiff))}%`
  713. ));
  714.  
  715. if (Math.abs(marketPriceDiff) < 0.5) {
  716. marketIndicator.classList.add('diff-equal');
  717. } else if (marketPriceDiff > 0) {
  718. if (marketPriceDiff >= 90) marketIndicator.classList.add('diff-90-100');
  719. else if (marketPriceDiff >= 60) marketIndicator.classList.add('diff-60-90');
  720. else if (marketPriceDiff >= 30) marketIndicator.classList.add('diff-30-60');
  721. else marketIndicator.classList.add('diff-0-30');
  722. } else {
  723. if (marketPriceDiff <= -90) marketIndicator.classList.add('diff90-plus');
  724. else if (marketPriceDiff <= -60) marketIndicator.classList.add('diff60-90');
  725. else if (marketPriceDiff <= -30) marketIndicator.classList.add('diff30-60');
  726. else marketIndicator.classList.add('diff0-30');
  727. }
  728.  
  729. indicatorsRow.appendChild(marketIndicator);
  730. }
  731.  
  732. // Only add the row if we have at least one indicator
  733. if (indicatorsRow.children.length > 0) {
  734. container.after(indicatorsRow);
  735. }
  736. }
  737.  
  738. function updateSingleElement(element) {
  739. let itemId, priceElement;
  740.  
  741. // Check if we're in mobile view
  742. const isMobileView = window.innerWidth < 785;
  743.  
  744. if (isMobileView) {
  745. // Find item ID from info button's aria-controls
  746. const infoButton = document.querySelector('button[aria-controls^="wai-itemInfo-"]');
  747. if (infoButton) {
  748. const ariaControls = infoButton.getAttribute('aria-controls');
  749. const match = ariaControls.match(/wai-itemInfo-(\d+)/);
  750. if (match) itemId = match[1];
  751. }
  752.  
  753. if (element.classList.contains('price___v8rRx')) {
  754. priceElement = element;
  755. }
  756. } else {
  757. let container = element;
  758. while (container && !itemId) {
  759. const img = container.querySelector('img[src*="/images/items/"]');
  760. if (img) {
  761. const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
  762. if (idMatch) itemId = idMatch[1];
  763. }
  764. container = container.parentElement;
  765. }
  766.  
  767. if (element.classList.contains('priceAndTotal___eEVS7') ||
  768. element.classList.contains('price___Uwiv2') ||
  769. element.className.includes('price_')) {
  770. priceElement = element;
  771. }
  772. }
  773.  
  774. if (!itemId || !priceElement) return;
  775.  
  776. const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
  777. if (priceMatch) {
  778. const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
  779. addPriceIndicator(itemId, itemPrice, priceElement);
  780. }
  781. }
  782.  
  783. function processElements() {
  784. const isMobileView = window.innerWidth < 785;
  785.  
  786. if (document.URL.includes('sid=ItemMarket')) {
  787. // Item tiles - keep original handling for both mobile and desktop
  788. document.querySelectorAll('.itemTile___cbw7w').forEach(tile => {
  789. const img = tile.querySelector('img.torn-item');
  790. if (!img) return;
  791.  
  792. const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
  793. if (!idMatch) return;
  794.  
  795. const itemId = idMatch[1];
  796. const priceElement = tile.querySelector('.priceAndTotal___eEVS7');
  797.  
  798. if (priceElement) {
  799. const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
  800. if (priceMatch) {
  801. const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
  802. addPriceIndicator(itemId, itemPrice, priceElement);
  803. }
  804. }
  805. });
  806.  
  807. // Seller rows - handle differently for mobile vs desktop
  808. if (isMobileView) {
  809. const infoButton = document.querySelector('button[aria-controls^="wai-itemInfo-"]');
  810. if (infoButton) {
  811. const ariaControls = infoButton.getAttribute('aria-controls');
  812. const match = ariaControls.match(/wai-itemInfo-(\d+)/);
  813. if (match) {
  814. const itemId = match[1];
  815. document.querySelectorAll('.sellerRow___Ca2pK').forEach(row => {
  816. const priceElement = row.querySelector('.price___v8rRx');
  817. if (priceElement) {
  818. const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
  819. if (priceMatch) {
  820. const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
  821. addPriceIndicator(itemId, itemPrice, priceElement);
  822. // Restructure mobile layout
  823. if (!row.querySelector('.userInfoHead___LXxjB')) { // Skip header row
  824. const indicatorsRow = row.querySelector('.price-indicators-row');
  825. if (indicatorsRow) {
  826. priceElement.appendChild(indicatorsRow);
  827. }
  828. }
  829. }
  830. }
  831. });
  832. }
  833. }
  834. } else {
  835. document.querySelectorAll('.sellerRow___AI0m6').forEach(row => {
  836. const img = row.querySelector('.thumbnail___M_h9v img');
  837. if (!img) return;
  838.  
  839. const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
  840. if (!idMatch) return;
  841.  
  842. const itemId = idMatch[1];
  843. const priceElement = row.querySelector('.price___Uwiv2');
  844.  
  845. if (priceElement) {
  846. const priceText = priceElement.textContent;
  847. const priceMatch = priceText.match(/\$([0-9,]+)/);
  848. if (priceMatch) {
  849. const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
  850. addPriceIndicator(itemId, itemPrice, priceElement);
  851. }
  852. }
  853. });
  854. }
  855. }
  856. else if (document.URL.includes('bazaar.php')) {
  857. document.querySelectorAll('img[src*="/images/items/"][src*="/large.png"]').forEach(img => {
  858. if (!img.parentElement?.parentElement?.parentElement) return;
  859.  
  860. const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
  861. if (!idMatch) return;
  862.  
  863. const itemId = idMatch[1];
  864. const container = img.parentElement.parentElement.parentElement;
  865. const priceElement = container.querySelector('[class*="price_"]');
  866.  
  867. if (priceElement) {
  868. const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
  869. if (priceMatch) {
  870. const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
  871. addPriceIndicator(itemId, itemPrice, priceElement);
  872. }
  873. }
  874. });
  875. }
  876. }
  877.  
  878. function initialize() {
  879. const lastUpdate = GM_getValue("lastUpdate", 0);
  880. const lastMarketUpdate = GM_getValue("lastMarketUpdate", 0);
  881. const now = Date.now();
  882.  
  883. if (now - lastUpdate > 24 * 60 * 60 * 1000) {
  884. getAWHPrices();
  885. }
  886.  
  887. if (now - lastMarketUpdate > 24 * 60 * 60 * 1000) {
  888. getTornMarketValues();
  889. }
  890.  
  891. try {
  892. torn_market_values = JSON.parse(GM_getValue("Torn_Market_Values", "{}"));
  893. } catch (e) {}
  894.  
  895. scheduleNextUpdate();
  896.  
  897. setTimeout(() => {
  898. observer.observe(document.body, {
  899. childList: true,
  900. subtree: true,
  901. characterData: true,
  902. characterDataOldValue: true,
  903. attributes: true,
  904. attributeFilter: ['class']
  905. });
  906. processElements();
  907. }, 1000);
  908.  
  909. createFloatingContainer();
  910. }
  911.  
  912. const observer = new MutationObserver(mutations => {
  913. let affected = new Set();
  914.  
  915. for (const mutation of mutations) {
  916. if (mutation.type === 'characterData') {
  917. let parentElement = mutation.target.parentElement;
  918. while (parentElement) {
  919. if (parentElement.classList) {
  920. if (parentElement.classList.contains('priceAndTotal___eEVS7') ||
  921. parentElement.classList.contains('price___Uwiv2') ||
  922. [...parentElement.classList].some(c => c.includes('price_'))) {
  923. affected.add(parentElement);
  924. break;
  925. }
  926. }
  927. parentElement = parentElement.parentElement;
  928. }
  929. }
  930. else if (mutation.addedNodes.length) {
  931. for (const node of mutation.addedNodes) {
  932. if (node.nodeType === Node.ELEMENT_NODE) {
  933. if (node.classList?.contains('itemTile___cbw7w') ||
  934. node.classList?.contains('sellerRow___AI0m6') ||
  935. node.querySelector?.('.itemTile___cbw7w, .sellerRow___AI0m6, [class*="price_"]')) {
  936. processElements();
  937. return;
  938. }
  939. }
  940. }
  941. }
  942. }
  943.  
  944. affected.forEach(element => updateSingleElement(element));
  945. });
  946.  
  947. // Start the script
  948. initialize();
  949.  
  950. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址