Bazaars in Item Market powered by TornPal and IronNerd

Displays bazaar listings with sorting controls via TornPal & IronNerd

目前为 2025-03-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Bazaars in Item Market powered by TornPal and IronNerd
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.33
  5. // @description Displays bazaar listings with sorting controls via TornPal & IronNerd
  6. // @author Weav3r
  7. // @match https://www.torn.com/*
  8. // @grant GM.xmlHttpRequest
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_deleteValue
  12. // @grant GM_listValues
  13. // @grant GM.setValue
  14. // @grant GM.getValue
  15. // @grant GM.deleteValue
  16. // @grant GM.listValues
  17. // @connect tornpal.com
  18. // @connect www.ironnerd.me
  19. // @run-at document-end
  20. // ==/UserScript==
  21.  
  22. (function () {
  23. 'use strict';
  24.  
  25. if (typeof GM_setValue === 'undefined' && typeof GM !== 'undefined') {
  26. const GM_getValue = function(key, defaultValue) {
  27. let value;
  28. try {
  29. value = localStorage.getItem('GMcompat_' + key);
  30. if (value !== null) {
  31. return JSON.parse(value);
  32. }
  33. GM.getValue(key, defaultValue).then(val => {
  34. if (val !== undefined) {
  35. localStorage.setItem('GMcompat_' + key, JSON.stringify(val));
  36. }
  37. });
  38. return defaultValue;
  39. } catch (e) {
  40. console.error('Error in GM_getValue compatibility:', e);
  41. return defaultValue;
  42. }
  43. };
  44. const GM_setValue = function(key, value) {
  45. try {
  46. // Store in both localStorage and GM.setValue
  47. localStorage.setItem('GMcompat_' + key, JSON.stringify(value));
  48. GM.setValue(key, value);
  49. } catch (e) {
  50. console.error('Error in GM_setValue compatibility:', e);
  51. }
  52. };
  53. const GM_deleteValue = function(key) {
  54. try {
  55. localStorage.removeItem('GMcompat_' + key);
  56. GM.deleteValue(key);
  57. } catch (e) {
  58. console.error('Error in GM_deleteValue compatibility:', e);
  59. }
  60. };
  61. const GM_listValues = function() {
  62. // This is an approximation - we can only list keys with our prefix
  63. const keys = [];
  64. try {
  65. for (let i = 0; i < localStorage.length; i++) {
  66. const key = localStorage.key(i);
  67. if (key.startsWith('GMcompat_')) {
  68. keys.push(key.substring(9)); // Remove the prefix
  69. }
  70. }
  71. } catch (e) {
  72. console.error('Error in GM_listValues compatibility:', e);
  73. }
  74. return keys;
  75. };
  76. window.GM_getValue = GM_getValue;
  77. window.GM_setValue = GM_setValue;
  78. window.GM_deleteValue = GM_deleteValue;
  79. window.GM_listValues = GM_listValues;
  80. }
  81.  
  82. const CACHE_DURATION_MS = 60000,
  83. CARD_WIDTH = 180;
  84.  
  85. let currentSortKey = "price",
  86. currentSortOrder = "asc",
  87. allListings = [],
  88. currentDarkMode = document.body.classList.contains('dark-mode'),
  89. currentItemName = "",
  90. displayMode = "percentage",
  91. isMobileView = false;
  92.  
  93. const scriptSettings = {
  94. defaultSort: "price",
  95. defaultOrder: "asc",
  96. apiKey: "",
  97. listingFee: parseFloat(GM_getValue("bazaarListingFee") || "0"),
  98. defaultDisplayMode: "percentage",
  99. linkBehavior: GM_getValue("bazaarLinkBehavior") || "new_tab"
  100. };
  101.  
  102. const updateStyles = () => {
  103. let styleEl = document.getElementById('bazaar-enhanced-styles');
  104.  
  105. if (!styleEl) {
  106. styleEl = document.createElement('style');
  107. styleEl.id = 'bazaar-enhanced-styles';
  108. document.head.appendChild(styleEl);
  109. }
  110.  
  111. styleEl.textContent = `
  112. .bazaar-profit-tooltip {
  113. position: fixed;
  114. background: ${currentDarkMode ? '#333' : '#fff'};
  115. color: ${currentDarkMode ? '#fff' : '#333'};
  116. border: 1px solid ${currentDarkMode ? '#555' : '#ddd'};
  117. padding: 10px 14px;
  118. border-radius: 5px;
  119. box-shadow: 0 3px 10px rgba(0,0,0,0.3);
  120. z-index: 99999;
  121. min-width: 200px;
  122. max-width: 280px;
  123. width: auto;
  124. pointer-events: none;
  125. transition: opacity 0.2s ease;
  126. font-size: 13px;
  127. line-height: 1.4;
  128. }
  129.  
  130. @media (max-width: 768px) {
  131. .bazaar-profit-tooltip {
  132. font-size: 12px;
  133. max-width: 260px;
  134. padding: 8px 12px;
  135. }
  136. }
  137. `;
  138. };
  139.  
  140. updateStyles();
  141.  
  142. const darkModeObserver = new MutationObserver((mutations) => {
  143. mutations.forEach((mutation) => {
  144. if (mutation.attributeName === 'class') {
  145. const newDarkMode = document.body.classList.contains('dark-mode');
  146. if (newDarkMode !== currentDarkMode) {
  147. currentDarkMode = newDarkMode;
  148. updateStyles();
  149. }
  150. }
  151. });
  152. });
  153. darkModeObserver.observe(document.body, { attributes: true });
  154.  
  155. function checkMobileView() {
  156. isMobileView = window.innerWidth < 784;
  157. return isMobileView;
  158. }
  159. checkMobileView();
  160. window.addEventListener('resize', function() {
  161. checkMobileView();
  162. processMobileSellerList();
  163. });
  164.  
  165. function loadSettings() {
  166. try {
  167. const saved = GM_getValue("bazaarsSettings");
  168. if (saved) {
  169. const parsedSettings = JSON.parse(saved);
  170.  
  171. Object.assign(scriptSettings, parsedSettings);
  172. if (parsedSettings.defaultSort) {
  173. currentSortKey = parsedSettings.defaultSort;
  174. }
  175. if (parsedSettings.defaultOrder) {
  176. currentSortOrder = parsedSettings.defaultOrder;
  177. }
  178. if (parsedSettings.defaultDisplayMode) {
  179. displayMode = parsedSettings.defaultDisplayMode;
  180. }
  181. }
  182. } catch (e) {
  183. console.error("Oops, settings failed to load:", e);
  184. }
  185. }
  186.  
  187. function saveSettings() {
  188. try {
  189. GM_setValue("bazaarsSettings", JSON.stringify(scriptSettings));
  190. GM_setValue("bazaarApiKey", scriptSettings.apiKey || "");
  191. GM_setValue("bazaarDefaultSort", scriptSettings.defaultSort || "price");
  192. GM_setValue("bazaarDefaultOrder", scriptSettings.defaultOrder || "asc");
  193. GM_setValue("bazaarListingFee", scriptSettings.listingFee || 0);
  194. GM_setValue("bazaarDefaultDisplayMode", scriptSettings.defaultDisplayMode || "percentage");
  195. GM_setValue("bazaarLinkBehavior", scriptSettings.linkBehavior || "new_tab");
  196. } catch (e) {
  197. console.error("Settings save hiccup:", e);
  198. }
  199. }
  200. loadSettings();
  201.  
  202. const style = document.createElement("style");
  203. style.textContent = `
  204. .bazaar-button {
  205. padding: 3px 6px;
  206. border: 1px solid #ccc;
  207. border-radius: 4px;
  208. background-color: #fff;
  209. color: #000;
  210. cursor: pointer;
  211. font-size: 12px;
  212. margin-left: 4px;
  213. }
  214. .dark-mode .bazaar-button {
  215. border: 1px solid #444;
  216. background-color: #1a1a1a;
  217. color: #fff;
  218. }
  219. .bazaar-modal-overlay {
  220. position: fixed;
  221. top: 0;
  222. left: 0;
  223. width: 100%;
  224. height: 100%;
  225. background-color: rgba(0,0,0,0.7);
  226. display: flex;
  227. justify-content: center;
  228. align-items: center;
  229. z-index: 99999;
  230. }
  231. .bazaar-info-container {
  232. font-size: 13px;
  233. border-radius: 4px;
  234. margin: 5px 0;
  235. padding: 10px;
  236. display: flex;
  237. flex-direction: column;
  238. gap: 8px;
  239. background-color: #f9f9f9;
  240. color: #000;
  241. border: 1px solid #ccc;
  242. box-sizing: border-box;
  243. width: 100%;
  244. overflow: hidden;
  245. }
  246. .dark-mode .bazaar-info-container {
  247. background-color: #2f2f2f;
  248. color: #ccc;
  249. border: 1px solid #444;
  250. }
  251. .bazaar-info-header {
  252. font-size: 16px;
  253. font-weight: bold;
  254. color: #000;
  255. }
  256. .dark-mode .bazaar-info-header {
  257. color: #fff;
  258. }
  259. .bazaar-sort-controls {
  260. display: flex;
  261. align-items: center;
  262. gap: 5px;
  263. font-size: 12px;
  264. padding: 5px;
  265. background-color: #eee;
  266. border-radius: 4px;
  267. border: 1px solid #ccc;
  268. }
  269. .dark-mode .bazaar-sort-controls {
  270. background-color: #333;
  271. border: 1px solid #444;
  272. }
  273. .bazaar-sort-select {
  274. padding: 3px 24px 3px 8px;
  275. border: 1px solid #ccc;
  276. border-radius: 4px;
  277. background: #fff url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iIzY2NiIvPjwvc3ZnPg==") no-repeat right 8px center;
  278. background-size: 10px 6px;
  279. -webkit-appearance: none;
  280. -moz-appearance: none;
  281. appearance: none;
  282. cursor: pointer;
  283. }
  284. .bazaar-profit-tooltip {
  285. position: fixed;
  286. background: #fff;
  287. color: #333;
  288. border: 1px solid #ddd;
  289. padding: 8px 12px;
  290. border-radius: 4px;
  291. box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  292. z-index: 99999;
  293. min-width: 200px;
  294. max-width: 280px;
  295. width: auto;
  296. pointer-events: none;
  297. transition: opacity 0.2s ease;
  298. }
  299. .dark-mode .bazaar-profit-tooltip {
  300. background: #333;
  301. color: #fff;
  302. border: 1px solid #555;
  303. }
  304. .dark-mode .bazaar-sort-select {
  305. border: 1px solid #444;
  306. background-color: #1a1a1a;
  307. color: #fff;
  308. background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iI2NjYyIvPjwvc3ZnPg==");
  309. }
  310. .bazaar-sort-select:focus {
  311. outline: none;
  312. border-color: #0078d7;
  313. box-shadow: 0 0 0 1px #0078d7;
  314. }
  315. .bazaar-min-qty {
  316. background-color: #fff;
  317. color: #000;
  318. font-size: 12px;
  319. }
  320. .dark-mode .bazaar-min-qty {
  321. border: 1px solid #444 !important;
  322. background-color: #1a1a1a;
  323. color: #fff;
  324. }
  325. .bazaar-min-qty:focus {
  326. outline: none;
  327. border-color: #0078d7 !important;
  328. box-shadow: 0 0 0 1px #0078d7;
  329. }
  330. .bazaar-scroll-container {
  331. position: relative;
  332. display: flex;
  333. align-items: stretch;
  334. width: 100%;
  335. box-sizing: border-box;
  336. }
  337. .bazaar-scroll-wrapper {
  338. flex: 1;
  339. overflow-x: auto;
  340. overflow-y: hidden;
  341. height: 100px;
  342. white-space: nowrap;
  343. padding-bottom: 3px;
  344. border-radius: 4px;
  345. border: 1px solid #ccc;
  346. margin: 0 auto;
  347. max-width: calc(100% - 30px);
  348. position: relative;
  349. }
  350. .dark-mode .bazaar-scroll-wrapper {
  351. border: 1px solid #444;
  352. }
  353. .bazaar-scroll-arrow {
  354. display: flex;
  355. align-items: center;
  356. justify-content: center;
  357. width: 12px;
  358. flex-shrink: 0;
  359. flex-grow: 0;
  360. cursor: pointer;
  361. background-color: transparent;
  362. border: none;
  363. opacity: 0.5;
  364. transition: opacity 0.2s ease;
  365. margin: 0 1px;
  366. z-index: 2;
  367. position: relative;
  368. }
  369. .bazaar-scroll-arrow:hover {
  370. opacity: 0.9;
  371. background-color: transparent;
  372. }
  373. .dark-mode .bazaar-scroll-arrow {
  374. background-color: transparent;
  375. border: none;
  376. }
  377. .bazaar-scroll-arrow svg {
  378. width: 18px !important;
  379. height: 18px !important;
  380. color: #888;
  381. }
  382. .dark-mode .bazaar-scroll-arrow svg {
  383. color: #777;
  384. }
  385. .bazaar-scroll-arrow.left {
  386. padding-left: 10px;
  387. margin-left: -10px;
  388. }
  389. .bazaar-scroll-arrow.right {
  390. padding-right: 10px;
  391. margin-right: -10px;
  392. }
  393. .bazaar-scroll-wrapper::-webkit-scrollbar {
  394. height: 8px;
  395. }
  396. .bazaar-scroll-wrapper::-webkit-scrollbar-track {
  397. background: #f1f1f1;
  398. }
  399. .bazaar-scroll-wrapper::-webkit-scrollbar-thumb {
  400. background: #888;
  401. border-radius: 4px;
  402. }
  403. .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover {
  404. background: #555;
  405. }
  406. .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-track {
  407. background: #333;
  408. }
  409. .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb {
  410. background: #555;
  411. }
  412. .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover {
  413. background: #777;
  414. }
  415. .bazaar-card-container {
  416. position: relative;
  417. height: 100%;
  418. display: flex;
  419. align-items: center;
  420. }
  421. .bazaar-listing-card {
  422. position: absolute;
  423. min-width: 140px;
  424. max-width: 200px;
  425. display: flex;
  426. flex-direction: column;
  427. justify-content: space-between;
  428. border-radius: 4px;
  429. padding: 8px;
  430. font-size: clamp(12px, 1vw, 14px);
  431. box-sizing: border-box;
  432. overflow: hidden;
  433. background-color: #fff;
  434. color: #000;
  435. border: 1px solid #ccc;
  436. top: 50%;
  437. transform: translateY(-50%);
  438. word-break: break-word;
  439. height: auto;
  440. /* Added transition for position, opacity and scale */
  441. transition: left 0.5s ease, opacity 0.5s ease, transform 0.5s ease;
  442. }
  443. .dark-mode .bazaar-listing-card {
  444. background-color: #1a1a1a;
  445. color: #fff;
  446. border: 1px solid #444;
  447. }
  448. /* Fade-in/out classes for animations */
  449. .fade-in {
  450. opacity: 0;
  451. transform: translateY(-50%) scale(0.8);
  452. }
  453. .fade-out {
  454. opacity: 0;
  455. transform: translateY(-50%) scale(0.8);
  456. }
  457. .bazaar-listing-footnote {
  458. font-size: 11px;
  459. text-align: right;
  460. color: #555;
  461. }
  462. .dark-mode .bazaar-listing-footnote {
  463. color: #aaa;
  464. }
  465. .bazaar-listing-source {
  466. font-size: 10px;
  467. text-align: right;
  468. color: #555;
  469. }
  470. .dark-mode .bazaar-listing-source {
  471. color: #aaa;
  472. }
  473. .bazaar-footer-container {
  474. display: flex;
  475. justify-content: space-between;
  476. align-items: center;
  477. margin-top: 5px;
  478. font-size: 10px;
  479. }
  480. .bazaar-powered-by span {
  481. color: #999;
  482. }
  483. .dark-mode .bazaar-powered-by span {
  484. color: #666;
  485. }
  486. .bazaar-powered-by a {
  487. text-decoration: underline;
  488. color: #555;
  489. }
  490. .dark-mode .bazaar-powered-by a {
  491. color: #aaa;
  492. }
  493. @keyframes popAndFlash {
  494. 0% { transform: scale(1); background-color: rgba(0,255,0,0.6); }
  495. 50% { transform: scale(1.05); }
  496. 100% { transform: scale(1); background-color: inherit; }
  497. }
  498. .pop-flash {
  499. animation: popAndFlash 0.8s ease-in-out forwards;
  500. }
  501. .green-outline {
  502. border: 3px solid green !important;
  503. }
  504. .bazaar-settings-modal {
  505. background-color: #fff;
  506. border-radius: 8px;
  507. padding: 24px;
  508. width: 500px;
  509. max-width: 95vw;
  510. max-height: 90vh;
  511. overflow-y: auto;
  512. box-shadow: 0 4px 20px rgba(0,0,0,0.3);
  513. position: relative;
  514. z-index: 100000;
  515. font-family: 'Arial', sans-serif;
  516. }
  517. .dark-mode .bazaar-settings-modal {
  518. background-color: #2a2a2a;
  519. color: #e0e0e0;
  520. border: 1px solid #444;
  521. }
  522. .bazaar-settings-title {
  523. font-size: 20px;
  524. font-weight: bold;
  525. margin-bottom: 20px;
  526. color: #333;
  527. }
  528. .dark-mode .bazaar-settings-title {
  529. color: #fff;
  530. }
  531. .bazaar-tabs {
  532. display: flex;
  533. border-bottom: 1px solid #ddd;
  534. margin-bottom: 20px;
  535. padding-bottom: 0;
  536. flex-wrap: wrap;
  537. }
  538. .dark-mode .bazaar-tabs {
  539. border-bottom: 1px solid #444;
  540. }
  541. .bazaar-tab {
  542. padding: 10px 16px;
  543. cursor: pointer;
  544. margin-right: 5px;
  545. margin-bottom: 5px;
  546. border: 1px solid transparent;
  547. border-bottom: none;
  548. border-radius: 4px 4px 0 0;
  549. font-weight: normal;
  550. background-color: #f5f5f5;
  551. color: #555;
  552. position: relative;
  553. bottom: -1px;
  554. }
  555. .dark-mode .bazaar-tab {
  556. background-color: #333;
  557. color: #ccc;
  558. }
  559. .bazaar-tab.active {
  560. background-color: #fff;
  561. color: #333;
  562. border-color: #ddd;
  563. font-weight: bold;
  564. padding-bottom: 11px;
  565. }
  566. .dark-mode .bazaar-tab.active {
  567. background-color: #2a2a2a;
  568. color: #fff;
  569. border-color: #444;
  570. }
  571. .bazaar-tab-content {
  572. display: none;
  573. }
  574. .bazaar-tab-content.active {
  575. display: block;
  576. }
  577. .bazaar-settings-group {
  578. margin-bottom: 20px;
  579. }
  580. .bazaar-settings-item {
  581. margin-bottom: 18px;
  582. }
  583. .bazaar-settings-item label {
  584. display: block;
  585. margin-bottom: 8px;
  586. font-weight: bold;
  587. font-size: 14px;
  588. }
  589. .bazaar-settings-item input[type="text"],
  590. .bazaar-settings-item select,
  591. .bazaar-number-input {
  592. width: 100%;
  593. padding: 8px 12px;
  594. border: 1px solid #ccc;
  595. border-radius: 4px;
  596. font-size: 14px;
  597. background-color: #fff;
  598. color: #333;
  599. max-width: 200px;
  600. }
  601. .dark-mode .bazaar-settings-item input[type="text"],
  602. .dark-mode .bazaar-settings-item select,
  603. .dark-mode .bazaar-number-input {
  604. border: 1px solid #444;
  605. background-color: #222;
  606. color: #e0e0e0;
  607. }
  608. .bazaar-settings-item select {
  609. max-width: 200px;
  610. }
  611. .bazaar-number-input {
  612. -moz-appearance: textfield;
  613. appearance: textfield;
  614. width: 60px !important;
  615. }
  616. .bazaar-number-input::-webkit-outer-spin-button,
  617. .bazaar-number-input::-webkit-inner-spin-button {
  618. -webkit-appearance: none;
  619. margin: 0;
  620. }
  621. .bazaar-api-note {
  622. font-size: 12px;
  623. margin-top: 6px;
  624. color: #666;
  625. line-height: 1.4;
  626. }
  627. .dark-mode .bazaar-api-note {
  628. color: #aaa;
  629. }
  630. .bazaar-script-item {
  631. margin-bottom: 16px;
  632. padding-bottom: 16px;
  633. border-bottom: 1px solid #eee;
  634. }
  635. .dark-mode .bazaar-script-item {
  636. border-bottom: 1px solid #333;
  637. }
  638. .bazaar-script-item:last-child {
  639. border-bottom: none;
  640. }
  641. .bazaar-script-name {
  642. font-weight: bold;
  643. font-size: 16px;
  644. margin-bottom: 5px;
  645. }
  646. .bazaar-script-desc {
  647. margin-bottom: 8px;
  648. line-height: 1.4;
  649. color: #555;
  650. }
  651. .dark-mode .bazaar-script-desc {
  652. color: #bbb;
  653. }
  654. .bazaar-script-link {
  655. display: inline-block;
  656. margin-top: 5px;
  657. color: #2196F3;
  658. text-decoration: none;
  659. }
  660. .bazaar-script-link:hover {
  661. text-decoration: underline;
  662. }
  663. .bazaar-changelog {
  664. margin-bottom: 20px;
  665. }
  666. .bazaar-changelog-version {
  667. font-weight: bold;
  668. margin-bottom: 8px;
  669. font-size: 15px;
  670. }
  671. .bazaar-changelog-date {
  672. font-style: italic;
  673. color: #666;
  674. font-size: 13px;
  675. margin-bottom: 5px;
  676. }
  677. .dark-mode .bazaar-changelog-date {
  678. color: #aaa;
  679. }
  680. .bazaar-changelog-list {
  681. margin-left: 20px;
  682. margin-bottom: 15px;
  683. }
  684. .bazaar-changelog-item {
  685. margin-bottom: 5px;
  686. line-height: 1.4;
  687. }
  688. .bazaar-credits {
  689. margin-top: 20px;
  690. padding-top: 15px;
  691. border-top: 1px solid #eee;
  692. }
  693. .dark-mode .bazaar-credits {
  694. border-top: 1px solid #444;
  695. }
  696. .bazaar-credits h3 {
  697. font-size: 16px;
  698. margin-bottom: 10px;
  699. }
  700. .bazaar-credits p {
  701. line-height: 1.4;
  702. margin-bottom: 8px;
  703. }
  704. .bazaar-provider {
  705. font-weight: bold;
  706. }
  707. .bazaar-settings-buttons {
  708. display: flex;
  709. justify-content: flex-end;
  710. gap: 12px;
  711. margin-top: 30px;
  712. }
  713. .bazaar-settings-save,
  714. .bazaar-settings-cancel {
  715. padding: 8px 16px;
  716. border: none;
  717. border-radius: 4px;
  718. font-size: 14px;
  719. cursor: pointer;
  720. font-weight: bold;
  721. }
  722. .bazaar-settings-save {
  723. background-color: #4CAF50;
  724. color: white;
  725. }
  726. .bazaar-settings-save:hover {
  727. background-color: #45a049;
  728. }
  729. .bazaar-settings-cancel {
  730. background-color: #f5f5f5;
  731. color: #333;
  732. border: 1px solid #ddd;
  733. }
  734. .dark-mode .bazaar-settings-cancel {
  735. background-color: #333;
  736. color: #e0e0e0;
  737. border: 1px solid #444;
  738. }
  739. .bazaar-settings-cancel:hover {
  740. background-color: #e9e9e9;
  741. }
  742. .dark-mode .bazaar-settings-cancel:hover {
  743. background-color: #444 !important;
  744. border-color: #555 !important;
  745. }
  746. .bazaar-settings-footer {
  747. margin-top: 20px;
  748. font-size: 12px;
  749. color: #777;
  750. text-align: center;
  751. padding-top: 15px;
  752. border-top: 1px solid #eee;
  753. }
  754. .dark-mode .bazaar-settings-footer {
  755. color: #999;
  756. border-top: 1px solid #444;
  757. }
  758. .bazaar-settings-footer a {
  759. color: #2196F3;
  760. text-decoration: none;
  761. }
  762. .bazaar-settings-footer a:hover {
  763. text-decoration: underline;
  764. }
  765. @media (max-width: 600px) {
  766. .bazaar-settings-modal {
  767. padding: 16px;
  768. width: 100%;
  769. max-width: 100%;
  770. border-radius: 0;
  771. max-height: 100vh;
  772. }
  773. .bazaar-settings-title {
  774. font-size: 18px;
  775. margin-bottom: 16px;
  776. }
  777. .bazaar-tab {
  778. padding: 8px 12px;
  779. font-size: 14px;
  780. }
  781. .bazaar-settings-item label {
  782. font-size: 13px;
  783. }
  784. .bazaar-settings-item input[type="text"],
  785. .bazaar-settings-item select,
  786. .bazaar-number-input {
  787. padding: 6px 10px;
  788. font-size: 13px;
  789. max-width: 100%;
  790. }
  791. .bazaar-settings-item {
  792. margin-bottom: 14px;
  793. }
  794. .bazaar-settings-save,
  795. .bazaar-settings-cancel {
  796. padding: 6px 12px;
  797. font-size: 13px;
  798. }
  799. .bazaar-api-note {
  800. font-size: 11px;
  801. }
  802. .bazaar-settings-buttons {
  803. margin-top: 20px;
  804. }
  805. .bazaar-settings-footer {
  806. font-size: 11px;
  807. }
  808. }
  809. `;
  810. document.head.appendChild(style);
  811.  
  812. function fetchJSON(url, callback) {
  813. let retryCount = 0;
  814. const MAX_RETRIES = 2;
  815. const TIMEOUT_MS = 10000;
  816. const RETRY_DELAY_MS = 2000;
  817.  
  818. function makeRequest(options) {
  819. if (typeof GM_xmlhttpRequest !== 'undefined') {
  820. return GM_xmlhttpRequest(options);
  821. } else if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') {
  822. return GM.xmlHttpRequest(options);
  823. } else {
  824. console.error('Neither GM_xmlhttpRequest nor GM.xmlHttpRequest are available');
  825. options.onerror && options.onerror(new Error('XMLHttpRequest API not available'));
  826. return null;
  827. }
  828. }
  829.  
  830. function attemptFetch() {
  831. let timeoutId = setTimeout(() => {
  832. console.warn(`Request to ${url} timed out, ${retryCount < MAX_RETRIES ? 'retrying...' : 'giving up.'}`);
  833. if (retryCount < MAX_RETRIES) {
  834. retryCount++;
  835. setTimeout(attemptFetch, RETRY_DELAY_MS);
  836. } else {
  837. callback(null);
  838. }
  839. }, TIMEOUT_MS);
  840.  
  841. makeRequest({
  842. method: 'GET',
  843. url,
  844. timeout: TIMEOUT_MS,
  845. onload: res => {
  846. clearTimeout(timeoutId);
  847. try {
  848. if (res.status >= 200 && res.status < 300) {
  849. callback(JSON.parse(res.responseText));
  850. } else {
  851. console.warn(`Request to ${url} failed with status ${res.status}`);
  852. if (retryCount < MAX_RETRIES) {
  853. retryCount++;
  854. setTimeout(attemptFetch, RETRY_DELAY_MS);
  855. } else {
  856. callback(null);
  857. }
  858. }
  859. } catch (e) {
  860. console.error(`Error parsing response from ${url}:`, e);
  861. callback(null);
  862. }
  863. },
  864. onerror: (error) => {
  865. clearTimeout(timeoutId);
  866. console.warn(`Request to ${url} failed:`, error);
  867. if (retryCount < MAX_RETRIES) {
  868. retryCount++;
  869. setTimeout(attemptFetch, RETRY_DELAY_MS);
  870. } else {
  871. callback(null);
  872. }
  873. },
  874. ontimeout: () => {
  875. clearTimeout(timeoutId);
  876. console.warn(`Request to ${url} timed out natively`);
  877. if (retryCount < MAX_RETRIES) {
  878. retryCount++;
  879. setTimeout(attemptFetch, RETRY_DELAY_MS);
  880. } else {
  881. callback(null);
  882. }
  883. }
  884. });
  885. }
  886. attemptFetch();
  887. }
  888.  
  889. let cachedItemsData = null;
  890. function getStoredItems() {
  891. if (cachedItemsData === null) {
  892. try {
  893. cachedItemsData = JSON.parse(GM_getValue("tornItems") || "{}");
  894. } catch (e) {
  895. cachedItemsData = {};
  896. console.error("Stored items got funky:", e);
  897. }
  898. }
  899. return cachedItemsData;
  900. }
  901.  
  902. function getCache(itemId) {
  903. try {
  904. const key = "tornBazaarCache_" + itemId,
  905. cached = GM_getValue(key);
  906. if (cached) {
  907. const payload = JSON.parse(cached);
  908. if (Date.now() - payload.timestamp < CACHE_DURATION_MS) return payload.data;
  909. }
  910. } catch (e) {}
  911. return null;
  912. }
  913.  
  914. function setCache(itemId, data) {
  915. try {
  916. GM_setValue("tornBazaarCache_" + itemId, JSON.stringify({ timestamp: Date.now(), data }));
  917. } catch (e) {}
  918. }
  919.  
  920. function getRelativeTime(ts) {
  921. const diffSec = Math.floor((Date.now() - ts * 1000) / 1000);
  922. if (diffSec < 60) return diffSec + 's ago';
  923. if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
  924. if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
  925. return Math.floor(diffSec / 86400) + 'd ago';
  926. }
  927.  
  928. const svgTemplates = {
  929. rightArrow: `<svg viewBox="0 0 320 512"><path fill="currentColor" d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>`,
  930. leftArrow: `<svg viewBox="0 0 320 512"><path fill="currentColor" d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>`,
  931. warningIcon: `<path fill="currentColor" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/>`,
  932. infoIcon: `<path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/>`
  933. };
  934.  
  935. function createListingCard(listing, index) {
  936. const card = document.createElement('div');
  937. card.className = 'bazaar-listing-card';
  938. card.dataset.index = index;
  939. const listingKey = listing.player_id + '-' + listing.price + '-' + listing.quantity;
  940. card.dataset.listingKey = listingKey;
  941. card.dataset.quantity = listing.quantity;
  942. card.style.position = "absolute";
  943. card.style.left = (index * CARD_WIDTH) + "px";
  944. card.style.width = CARD_WIDTH + "px";
  945.  
  946. let visitedColor = '#00aaff';
  947. try {
  948. const key = `visited_${listing.item_id}_${listing.player_id}`;
  949. const data = JSON.parse(GM_getValue(key));
  950. if (data && data.lastClickedUpdated >= listing.updated) {
  951. visitedColor = 'purple';
  952. }
  953. } catch (e) {}
  954.  
  955. const displayName = listing.player_name ? listing.player_name : `ID: ${listing.player_id}`;
  956. card.innerHTML = `
  957. <div>
  958. <div style="display:flex; align-items:center; gap:5px; margin-bottom:6px; flex-wrap:wrap">
  959. <a href="https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemId=${listing.item_id}&highlight=1#/"
  960. data-visited-key="visited_${listing.item_id}_${listing.player_id}"
  961. data-updated="${listing.updated}"
  962. ${scriptSettings.linkBehavior === 'new_tab' ? 'target="_blank" rel="noopener noreferrer"' : ''}
  963. style="font-weight:bold; color:${visitedColor}; text-decoration:underline;">
  964. Player: ${displayName}
  965. </a>
  966. </div>
  967. <div>
  968. <div style="margin-bottom:2px">
  969. <strong>Price:</strong> <span style="word-break:break-all;">$${listing.price.toLocaleString()}</span>
  970. </div>
  971. <div style="display:flex; align-items:center">
  972. <strong>Qty:</strong> <span style="margin-left:4px">${listing.quantity}</span>
  973. <span style="margin-left:auto">${getPriceComparisonHtml(listing.price, listing.quantity)}</span>
  974. </div>
  975. </div>
  976. </div>
  977. <div style="margin-top:6px">
  978. <div class="bazaar-listing-footnote">Updated: ${getRelativeTime(listing.updated)}</div>
  979. <div class="bazaar-listing-source">Source: ${listing.source === "ironnerd" ? "IronNerd" : (listing.source === "bazaar" ? "TornPal" : listing.source)}</div>
  980. </div>
  981. `;
  982.  
  983. const playerLink = card.querySelector('a');
  984. playerLink.addEventListener('click', (e) => {
  985. GM_setValue(playerLink.dataset.visitedKey, JSON.stringify({ lastClickedUpdated: listing.updated }));
  986. playerLink.style.color = 'purple';
  987. const behavior = scriptSettings.linkBehavior || 'new_tab';
  988. if (behavior !== 'same_tab') {
  989. e.preventDefault();
  990. if (behavior === 'new_window') {
  991. window.open(playerLink.href, '_blank', 'noopener,noreferrer,width=1200,height=800');
  992. } else {
  993. window.open(playerLink.href, '_blank', 'noopener,noreferrer');
  994. }
  995. }
  996. });
  997.  
  998. const priceComparison = card.querySelector('.bazaar-price-comparison');
  999. if (priceComparison) {
  1000. const tooltip = document.createElement('div');
  1001. tooltip.className = 'bazaar-profit-tooltip';
  1002. tooltip.style.display = 'none';
  1003. tooltip.style.opacity = '0';
  1004. tooltip.innerHTML = priceComparison.getAttribute('data-tooltip');
  1005.  
  1006. priceComparison.addEventListener('mouseenter', e => {
  1007. document.body.appendChild(tooltip);
  1008. tooltip.style.display = 'block';
  1009.  
  1010. // Position initially to measure size
  1011. tooltip.style.left = '0';
  1012. tooltip.style.top = '0';
  1013.  
  1014. // Get dimensions after adding to DOM
  1015. const rect = e.target.getBoundingClientRect();
  1016. const tooltipRect = tooltip.getBoundingClientRect();
  1017.  
  1018. // Calculate optimal position
  1019. let left = rect.left;
  1020. let top = rect.bottom + 5;
  1021.  
  1022. // Check horizontal overflow
  1023. if (left + tooltipRect.width > window.innerWidth) {
  1024. left = Math.max(5, window.innerWidth - tooltipRect.width - 5);
  1025. }
  1026.  
  1027. // Check vertical overflow and place above if needed
  1028. if (top + tooltipRect.height > window.innerHeight) {
  1029. top = Math.max(5, rect.top - tooltipRect.height - 5);
  1030. }
  1031.  
  1032. // Apply final position
  1033. tooltip.style.left = left + 'px';
  1034. tooltip.style.top = top + 'px';
  1035.  
  1036. // Fade in
  1037. requestAnimationFrame(() => {
  1038. tooltip.style.opacity = '1';
  1039. });
  1040. });
  1041.  
  1042. priceComparison.addEventListener('mouseleave', () => {
  1043. tooltip.style.opacity = '0';
  1044. // Remove after transition
  1045. setTimeout(() => {
  1046. if (tooltip.parentNode) tooltip.parentNode.removeChild(tooltip);
  1047. }, 200);
  1048. });
  1049. }
  1050.  
  1051. return card;
  1052. }
  1053.  
  1054. function getPriceComparisonHtml(listingPrice, quantity) {
  1055. try {
  1056. const stored = getStoredItems();
  1057. const match = Object.values(stored).find(item =>
  1058. item.name && item.name.toLowerCase() === currentItemName.toLowerCase());
  1059. if (match && match.market_value) {
  1060. const marketValue = Number(match.market_value),
  1061. priceDiff = listingPrice - marketValue,
  1062. percentDiff = ((listingPrice / marketValue) - 1) * 100,
  1063. listingFee = scriptSettings.listingFee || 0,
  1064. totalCost = listingPrice * quantity,
  1065. potentialRevenue = marketValue * quantity,
  1066. feeAmount = Math.ceil(potentialRevenue * (listingFee / 100)),
  1067. potentialProfit = potentialRevenue - totalCost - feeAmount,
  1068. minResellPrice = Math.ceil(listingPrice / (1 - (listingFee / 100)));
  1069.  
  1070. let color, text;
  1071. const absProfit = Math.abs(potentialProfit);
  1072. let abbrevValue = potentialProfit < 0 ? '-' : '';
  1073. if (absProfit >= 1000000) {
  1074. abbrevValue += '$' + (absProfit / 1000000).toFixed(1).replace(/\.0$/, '') + 'm';
  1075. } else if (absProfit >= 1000) {
  1076. abbrevValue += '$' + (absProfit / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
  1077. } else {
  1078. abbrevValue += '$' + absProfit;
  1079. }
  1080. if (potentialProfit > 0) {
  1081. color = currentDarkMode ? '#7fff7f' : '#006400';
  1082. text = displayMode === "percentage" ? `(${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`;
  1083. } else if (potentialProfit < 0) {
  1084. color = currentDarkMode ? '#ff7f7f' : '#8b0000';
  1085. text = displayMode === "percentage" ? `(+${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`;
  1086. } else {
  1087. color = currentDarkMode ? '#cccccc' : '#666666';
  1088. text = displayMode === "percentage" ? `(0%)` : `($0)`;
  1089. }
  1090.  
  1091. // Improved tooltip content focusing only on key information
  1092. const tooltipContent = `
  1093. <div style="font-weight:bold; font-size:13px; margin-bottom:6px; text-align:center;">
  1094. ${potentialProfit >= 0 ? 'PROFIT' : 'LOSS'}: ${potentialProfit >= 0 ? '$' : '-$'}${Math.abs(potentialProfit).toLocaleString()}
  1095. </div>
  1096. <hr style="margin: 4px 0; border-color: ${currentDarkMode ? '#444' : '#ddd'}">
  1097. <div>Total Cost: $${totalCost.toLocaleString()} (${quantity} item${quantity > 1 ? 's' : ''})</div>
  1098. ${listingFee > 0 ? `<div>Resale Fee: ${listingFee}% ($${feeAmount.toLocaleString()})</div>` : ''}
  1099. ${listingFee > 0 ? `<div style="margin-top:6px; font-weight:bold;">Min. Resell Price: $${minResellPrice.toLocaleString()}</div>` : ''}
  1100. `;
  1101. const span = document.createElement('span');
  1102. span.style.fontWeight = 'bold';
  1103. span.style.fontSize = '10px';
  1104. span.style.padding = '0 4px';
  1105. span.style.borderRadius = '2px';
  1106. span.style.color = color;
  1107. span.style.cursor = 'help';
  1108. span.style.whiteSpace = 'nowrap';
  1109. span.textContent = text;
  1110. span.className = 'bazaar-price-comparison';
  1111. span.setAttribute('data-tooltip', tooltipContent);
  1112. return span.outerHTML;
  1113. }
  1114. } catch (e) {
  1115. console.error("Price comparison error:", e);
  1116. }
  1117. return '';
  1118. }
  1119.  
  1120. function renderVirtualCards(infoContainer) {
  1121. const cardContainer = infoContainer.querySelector('.bazaar-card-container'),
  1122. scrollWrapper = infoContainer.querySelector('.bazaar-scroll-wrapper');
  1123. if (!cardContainer || !scrollWrapper || !infoContainer.isConnected) return;
  1124. try {
  1125. const minQtyInput = infoContainer.querySelector('.bazaar-min-qty');
  1126. const minQty = minQtyInput && minQtyInput.value ? parseInt(minQtyInput.value, 10) : 0;
  1127. if (!infoContainer.originalListings && allListings && allListings.length > 0) {
  1128. infoContainer.originalListings = [...allListings];
  1129. }
  1130. if ((!allListings || allListings.length === 0) && infoContainer.originalListings) {
  1131. allListings = [...infoContainer.originalListings];
  1132. }
  1133. const filteredListings = minQty > 0 ? allListings.filter(listing => listing.quantity >= minQty) : allListings;
  1134. if (filteredListings.length === 0 && allListings.length > 0) {
  1135. cardContainer.innerHTML = '';
  1136. const messageContainer = document.createElement('div');
  1137. messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;';
  1138. const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  1139. iconSvg.setAttribute("viewBox", "0 0 512 512");
  1140. iconSvg.setAttribute("width", "24");
  1141. iconSvg.setAttribute("height", "24");
  1142. iconSvg.style.marginBottom = "10px";
  1143. iconSvg.innerHTML = svgTemplates.infoIcon;
  1144. const textDiv = document.createElement('div');
  1145. textDiv.textContent = `No listings found with quantity ${minQty}. Try a lower value.`;
  1146. messageContainer.appendChild(iconSvg);
  1147. messageContainer.appendChild(textDiv);
  1148. cardContainer.appendChild(messageContainer);
  1149. const countElement = infoContainer.querySelector('.bazaar-listings-count');
  1150. if (countElement) {
  1151. countElement.textContent = `No listings match minimum quantity of ${minQty} (from ${allListings.length} total listings)`;
  1152. }
  1153. return;
  1154. }
  1155.  
  1156. if (cardContainer.style.width !== (filteredListings.length * CARD_WIDTH) + "px") {
  1157. cardContainer.style.width = (filteredListings.length * CARD_WIDTH) + "px";
  1158. }
  1159.  
  1160. const scrollLeft = scrollWrapper.scrollLeft,
  1161. containerWidth = scrollWrapper.clientWidth;
  1162. const visibleCards = Math.ceil(containerWidth / CARD_WIDTH),
  1163. buffer = Math.max(2, Math.floor(visibleCards / 3));
  1164. const totalItems = filteredListings.length;
  1165.  
  1166. if (infoContainer.lastRenderScrollLeft !== undefined &&
  1167. Math.abs(infoContainer.lastRenderScrollLeft - scrollLeft) < CARD_WIDTH * 0.3) {
  1168.  
  1169. }
  1170. infoContainer.lastRenderScrollLeft = scrollLeft;
  1171.  
  1172. let startIndex = Math.max(0, Math.floor(scrollLeft / CARD_WIDTH) - buffer),
  1173. endIndex = Math.min(totalItems, Math.ceil((scrollLeft + containerWidth) / CARD_WIDTH) + buffer);
  1174.  
  1175. const newVisible = {};
  1176. for (let i = startIndex; i < endIndex; i++) {
  1177. const listing = filteredListings[i];
  1178. const key = listing.player_id + '-' + listing.price + '-' + listing.quantity;
  1179. newVisible[key] = i;
  1180. }
  1181. Array.from(cardContainer.children).forEach(card => {
  1182. if (!card.classList.contains('bazaar-listing-card')) return;
  1183. const key = card.dataset.listingKey;
  1184. if (key in newVisible) {
  1185. const newIndex = newVisible[key];
  1186. card.dataset.index = newIndex;
  1187. card.style.left = (newIndex * CARD_WIDTH) + "px";
  1188. delete newVisible[key];
  1189. } else {
  1190. card.classList.add('fade-out');
  1191. card.addEventListener('transitionend', () => card.remove(), { once: true });
  1192. }
  1193. });
  1194. const fragment = document.createDocumentFragment();
  1195. for (const key in newVisible) {
  1196. const newIndex = newVisible[key];
  1197. const listing = filteredListings[newIndex];
  1198. const newCard = createListingCard(listing, newIndex);
  1199. newCard.classList.add('fade-in');
  1200. fragment.appendChild(newCard);
  1201. requestAnimationFrame(() => {
  1202. newCard.classList.remove('fade-in');
  1203. });
  1204. }
  1205. if (fragment.childElementCount > 0) {
  1206. cardContainer.appendChild(fragment);
  1207. }
  1208. const totalQuantity = filteredListings.reduce((sum, listing) => sum + listing.quantity, 0);
  1209. const countElement = infoContainer.querySelector('.bazaar-listings-count');
  1210. if (countElement) {
  1211. if (minQty > 0 && filteredListings.length < allListings.length) {
  1212. countElement.textContent = `Showing ${filteredListings.length} of ${allListings.length} bazaars (${totalQuantity.toLocaleString()} items total, min qty: ${minQty})`;
  1213. } else {
  1214. countElement.textContent = `Showing bazaars ${startIndex + 1}-${endIndex} of ${totalItems} (${totalQuantity.toLocaleString()} items total)`;
  1215. }
  1216. }
  1217. } catch (error) {
  1218. console.error("Error rendering virtual cards:", error);
  1219. }
  1220. }
  1221.  
  1222. function createInfoContainer(itemName, itemId) {
  1223. const container = document.createElement('div');
  1224. container.className = 'bazaar-info-container';
  1225. container.dataset.itemid = itemId;
  1226. currentItemName = itemName;
  1227. const header = document.createElement('div');
  1228. header.className = 'bazaar-info-header';
  1229. let marketValueText = "";
  1230. try {
  1231. const stored = getStoredItems();
  1232. const match = Object.values(stored).find(item =>
  1233. item.name && item.name.toLowerCase() === itemName.toLowerCase());
  1234. if (match && match.market_value) {
  1235. marketValueText = `Market Value: $${Number(match.market_value).toLocaleString()}`;
  1236. }
  1237. } catch (e) {
  1238. console.error("Header market value error:", e);
  1239. }
  1240. header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`;
  1241. if (marketValueText) {
  1242. const span = document.createElement('span');
  1243. span.style.marginLeft = '8px';
  1244. span.style.fontSize = '14px';
  1245. span.style.fontWeight = 'normal';
  1246. span.style.color = currentDarkMode ? '#aaa' : '#666';
  1247. span.textContent = `• ${marketValueText}`;
  1248. header.appendChild(span);
  1249. }
  1250. container.appendChild(header);
  1251. currentSortOrder = getSortOrderForKey(currentSortKey);
  1252. const sortControls = document.createElement('div');
  1253. sortControls.className = 'bazaar-sort-controls';
  1254. sortControls.innerHTML = `
  1255. <span>Sort by:</span>
  1256. <select class="bazaar-sort-select">
  1257. <option value="price" ${currentSortKey === "price" ? "selected" : ""}>Price</option>
  1258. <option value="quantity" ${currentSortKey === "quantity" ? "selected" : ""}>Quantity</option>
  1259. <option value="profit" ${currentSortKey === "profit" ? "selected" : ""}>Profit</option>
  1260. <option value="updated" ${currentSortKey === "updated" ? "selected" : ""}>Last Updated</option>
  1261. </select>
  1262. <button class="bazaar-button bazaar-order-toggle">
  1263. ${currentSortOrder === "asc" ? "Asc" : "Desc"}
  1264. </button>
  1265. <button class="bazaar-button bazaar-display-toggle" title="Toggle between percentage difference and total profit">
  1266. ${displayMode === "percentage" ? "%" : "$"}
  1267. </button>
  1268. <span style="margin-left: 8px;">Min Qty:</span>
  1269. <input type="number" class="bazaar-min-qty" style="width: 60px; padding: 3px; border: 1px solid #ccc; border-radius: 4px;" min="0" placeholder="">
  1270. `;
  1271. container.appendChild(sortControls);
  1272. const scrollContainer = document.createElement('div');
  1273. scrollContainer.className = 'bazaar-scroll-container';
  1274. function createScrollArrow(direction) {
  1275. const arrow = document.createElement('div');
  1276. arrow.className = `bazaar-scroll-arrow ${direction}`;
  1277. arrow.innerHTML = svgTemplates[direction === 'left' ? 'leftArrow' : 'rightArrow'];
  1278. let isScrolling = false,
  1279. scrollAnimationId = null,
  1280. startTime = 0,
  1281. isClickAction = false;
  1282. const ACTION_THRESHOLD = 200;
  1283. function smoothScroll() {
  1284. if (!isScrolling) return;
  1285. scrollWrapper.scrollLeft += (direction === 'left' ? -1.5 : 1.5);
  1286. scrollAnimationId = requestAnimationFrame(smoothScroll);
  1287. }
  1288. function startScrolling(e) {
  1289. e.preventDefault();
  1290. startTime = Date.now();
  1291. isClickAction = false;
  1292. setTimeout(() => {
  1293. if (startTime && Date.now() - startTime >= ACTION_THRESHOLD) {
  1294. isScrolling = true;
  1295. smoothScroll();
  1296. }
  1297. }, ACTION_THRESHOLD);
  1298. }
  1299. function stopScrolling() {
  1300. const holdDuration = Date.now() - startTime;
  1301. isScrolling = false;
  1302. if (scrollAnimationId) {
  1303. cancelAnimationFrame(scrollAnimationId);
  1304. scrollAnimationId = null;
  1305. }
  1306. if (holdDuration < ACTION_THRESHOLD && !isClickAction) {
  1307. isClickAction = true;
  1308. scrollWrapper.scrollBy({ left: direction === 'left' ? -200 : 200, behavior: 'smooth' });
  1309. }
  1310. startTime = 0;
  1311. }
  1312. arrow.addEventListener('mousedown', startScrolling);
  1313. arrow.addEventListener('mouseup', stopScrolling);
  1314. arrow.addEventListener('mouseleave', stopScrolling);
  1315. arrow.addEventListener('touchstart', startScrolling, { passive: false });
  1316. arrow.addEventListener('touchend', stopScrolling);
  1317. arrow.addEventListener('touchcancel', stopScrolling);
  1318. return arrow;
  1319. }
  1320. scrollContainer.appendChild(createScrollArrow('left'));
  1321. const scrollWrapper = document.createElement('div');
  1322. scrollWrapper.className = 'bazaar-scroll-wrapper';
  1323. const cardContainer = document.createElement('div');
  1324. cardContainer.className = 'bazaar-card-container';
  1325. scrollWrapper.appendChild(cardContainer);
  1326. scrollContainer.appendChild(scrollWrapper);
  1327. scrollContainer.appendChild(createScrollArrow('right'));
  1328. scrollWrapper.addEventListener('scroll', () => {
  1329. if (!scrollWrapper.isScrolling) {
  1330. scrollWrapper.isScrolling = true;
  1331. requestAnimationFrame(function checkScroll() {
  1332. renderVirtualCards(container);
  1333. if (scrollWrapper.lastKnownScrollLeft === scrollWrapper.scrollLeft) {
  1334. renderVirtualCards(container);
  1335. scrollWrapper.isScrolling = false;
  1336. } else {
  1337. scrollWrapper.lastKnownScrollLeft = scrollWrapper.scrollLeft;
  1338. requestAnimationFrame(checkScroll);
  1339. }
  1340. });
  1341. }
  1342. });
  1343. container.appendChild(scrollContainer);
  1344. const footerContainer = document.createElement('div');
  1345. footerContainer.className = 'bazaar-footer-container';
  1346. const listingsCount = document.createElement('div');
  1347. listingsCount.className = 'bazaar-listings-count';
  1348. listingsCount.textContent = 'Loading...';
  1349. footerContainer.appendChild(listingsCount);
  1350. const poweredBy = document.createElement('div');
  1351. poweredBy.className = 'bazaar-powered-by';
  1352. poweredBy.innerHTML = `
  1353. <span>Powered by </span>
  1354. <a href="https://tornpal.com/login?ref=1853324" target="_blank">TornPal</a>
  1355. <span> &amp; </span>
  1356. <a href="https://ironnerd.me/" target="_blank">IronNerd</a>
  1357. `;
  1358. footerContainer.appendChild(poweredBy);
  1359. container.appendChild(footerContainer);
  1360. return container;
  1361. }
  1362.  
  1363. function sortListings(listings) {
  1364. return listings.slice().sort((a, b) => {
  1365. let diff;
  1366. if (currentSortKey === "profit") {
  1367. try {
  1368. const stored = getStoredItems();
  1369. const match = Object.values(stored).find(item =>
  1370. item.name && item.name.toLowerCase() === currentItemName.toLowerCase());
  1371. if (match && match.market_value) {
  1372. const marketValue = Number(match.market_value),
  1373. fee = scriptSettings.listingFee || 0,
  1374. aProfit = (marketValue * a.quantity) - (a.price * a.quantity) - Math.ceil((marketValue * a.quantity) * (fee / 100)),
  1375. bProfit = (marketValue * b.quantity) - (b.price * b.quantity) - Math.ceil((marketValue * b.quantity) * (fee / 100));
  1376. diff = aProfit - bProfit;
  1377. } else {
  1378. diff = a.price - b.price;
  1379. }
  1380. } catch (e) {
  1381. console.error("Profit sort error:", e);
  1382. diff = a.price - b.price;
  1383. }
  1384. } else {
  1385. diff = currentSortKey === "price" ? a.price - b.price :
  1386. currentSortKey === "quantity" ? a.quantity - b.quantity :
  1387. a.updated - b.updated;
  1388. }
  1389. return currentSortOrder === "asc" ? diff : -diff;
  1390. });
  1391. }
  1392.  
  1393. function updateInfoContainer(wrapper, itemId, itemName) {
  1394. if (wrapper.hasAttribute('data-has-bazaar-info')) return;
  1395. let infoContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);
  1396. if (!infoContainer) {
  1397. infoContainer = createInfoContainer(itemName, itemId);
  1398. wrapper.insertBefore(infoContainer, wrapper.firstChild);
  1399. wrapper.setAttribute('data-has-bazaar-info', 'true');
  1400. } else if (!wrapper.contains(infoContainer)) {
  1401. infoContainer = createInfoContainer(itemName, itemId);
  1402. wrapper.insertBefore(infoContainer, wrapper.firstChild);
  1403. wrapper.setAttribute('data-has-bazaar-info', 'true');
  1404. } else {
  1405. const header = infoContainer.querySelector('.bazaar-info-header');
  1406. if (header) {
  1407. header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`;
  1408. }
  1409. }
  1410. const cardContainer = infoContainer.querySelector('.bazaar-card-container');
  1411. const countElement = infoContainer.querySelector('.bazaar-listings-count');
  1412. const updateListingsCount = (text) => {
  1413. if (countElement) {
  1414. countElement.textContent = text;
  1415. }
  1416. };
  1417. const showEmptyState = (isError) => {
  1418. if (cardContainer) {
  1419. cardContainer.innerHTML = '';
  1420. cardContainer.style.width = '';
  1421. renderMessageInContainer(cardContainer, isError);
  1422. }
  1423. updateListingsCount(isError ? 'API Error - Check back later' : 'No listings available');
  1424. };
  1425. if (cardContainer) {
  1426. cardContainer.innerHTML = '<div style="padding:10px; text-align:center; width:100%;">Loading bazaar listings...</div>';
  1427. }
  1428. const cachedData = getCache(itemId);
  1429. if (cachedData) {
  1430. allListings = sortListings(cachedData.listings);
  1431. if (allListings.length === 0) {
  1432. showEmptyState(false);
  1433. } else {
  1434. renderVirtualCards(infoContainer);
  1435. }
  1436. return;
  1437. }
  1438. let listings = [], responses = 0, apiErrors = false;
  1439. let requestTimeout = setTimeout(() => {
  1440. console.warn('Bazaar listings request timed out');
  1441. if (responses < 2) {
  1442. showEmptyState(true);
  1443. responses = 2;
  1444. }
  1445. }, 15000);
  1446. function processResponse(newListings, error) {
  1447. if (error) {
  1448. apiErrors = true;
  1449. }
  1450. if (Array.isArray(newListings)) {
  1451. newListings.forEach(newItem => {
  1452. const normalized = newItem.user_id !== undefined ? {
  1453. item_id: newItem.item_id,
  1454. player_id: newItem.user_id,
  1455. quantity: newItem.quantity,
  1456. price: newItem.price,
  1457. updated: newItem.last_updated,
  1458. source: "ironnerd",
  1459. player_name: newItem.player_name || null
  1460. } : newItem;
  1461. const duplicate = listings.find(item =>
  1462. item.player_id === normalized.player_id &&
  1463. item.price === normalized.price &&
  1464. item.quantity === normalized.quantity
  1465. );
  1466. if (duplicate) {
  1467. duplicate.source = duplicate.source === normalized.source ?
  1468. duplicate.source : "TornPal & IronNerd";
  1469. if (!duplicate.player_name && normalized.player_name) {
  1470. duplicate.player_name = normalized.player_name;
  1471. }
  1472. } else {
  1473. listings.push(normalized);
  1474. }
  1475. });
  1476. }
  1477. responses++;
  1478. if (responses === 2) {
  1479. clearTimeout(requestTimeout);
  1480. setCache(itemId, { listings });
  1481. if (listings.length === 0) {
  1482. showEmptyState(apiErrors);
  1483. } else {
  1484. allListings = sortListings(listings);
  1485. renderVirtualCards(infoContainer);
  1486. }
  1487. }
  1488. }
  1489. fetchJSON(`https://tornpal.com/api/v1/markets/clist/${itemId}?comment=wBazaarMarket`, data => {
  1490. processResponse(data && Array.isArray(data.listings) ? data.listings.filter(l => l.source === "bazaar") : [], data === null);
  1491. });
  1492. fetchJSON(`https://www.ironnerd.me/get_bazaar_items/${itemId}?comment=wBazaarMarket`, data => {
  1493. processResponse(data && Array.isArray(data.bazaar_items) ? data.bazaar_items : [], data === null);
  1494. });
  1495. }
  1496.  
  1497. function renderMessageInContainer(container, isApiError) {
  1498. container.innerHTML = '';
  1499. const messageContainer = document.createElement('div');
  1500. messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;';
  1501. const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  1502. iconSvg.setAttribute("viewBox", "0 0 512 512");
  1503. iconSvg.setAttribute("width", "24");
  1504. iconSvg.setAttribute("height", "24");
  1505. iconSvg.style.marginBottom = "10px";
  1506. const textDiv = document.createElement('div');
  1507. if (isApiError) {
  1508. iconSvg.innerHTML = svgTemplates.warningIcon;
  1509. textDiv.textContent = "Unable to load bazaar listings. Please try again later.";
  1510. textDiv.style.cssText = currentDarkMode ? 'color:#ff9999; font-weight:bold;' : 'color:#cc0000; font-weight:bold;';
  1511. } else {
  1512. iconSvg.innerHTML = svgTemplates.infoIcon;
  1513. textDiv.textContent = "No bazaar listings available for this item.";
  1514. }
  1515. messageContainer.appendChild(iconSvg);
  1516. messageContainer.appendChild(textDiv);
  1517. container.appendChild(messageContainer);
  1518. }
  1519.  
  1520. function processSellerWrapper(wrapper) {
  1521. if (!wrapper || wrapper.classList.contains('bazaar-info-container') || wrapper.hasAttribute('data-bazaar-processed')) return;
  1522. const existingContainer = wrapper.querySelector(':scope > .bazaar-info-container');
  1523. if (existingContainer) return;
  1524. const itemTile = wrapper.previousElementSibling;
  1525. if (!itemTile) return;
  1526. const nameEl = itemTile.querySelector('.name___ukdHN'),
  1527. btn = itemTile.querySelector('button[aria-controls^="wai-itemInfo-"]');
  1528. if (nameEl && btn) {
  1529. const itemName = nameEl.textContent.trim();
  1530. const idParts = btn.getAttribute('aria-controls').split('-');
  1531. const itemId = idParts[idParts.length - 1];
  1532. wrapper.setAttribute('data-bazaar-processed', 'true');
  1533. updateInfoContainer(wrapper, itemId, itemName);
  1534. }
  1535. }
  1536.  
  1537. function processMobileSellerList() {
  1538. if (!checkMobileView()) return;
  1539. const sellerList = document.querySelector('ul.sellerList___e4C9_, ul[class*="sellerList"]');
  1540. if (!sellerList) {
  1541. const existing = document.querySelector('.bazaar-info-container');
  1542. if (existing && !document.contains(existing.parentNode)) {
  1543. existing.remove();
  1544. }
  1545. return;
  1546. }
  1547. if (sellerList.hasAttribute('data-has-bazaar-container')) {
  1548. return;
  1549. }
  1550. const headerEl = document.querySelector('.itemsHeader___ZTO9r .title___ruNCT, [class*="itemsHeader"] [class*="title"]');
  1551. const itemName = headerEl ? headerEl.textContent.trim() : "Unknown";
  1552. const btn = document.querySelector('.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"], [class*="itemsHeader"] button[aria-controls^="wai-itemInfo-"]');
  1553. let itemId = "unknown";
  1554. if (btn) {
  1555. const parts = btn.getAttribute('aria-controls').split('-');
  1556. itemId = parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1];
  1557. }
  1558. const existingContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);
  1559. if (existingContainer) {
  1560. if (existingContainer.parentNode !== sellerList.parentNode ||
  1561. existingContainer.nextSibling !== sellerList) {
  1562. sellerList.parentNode.insertBefore(existingContainer, sellerList);
  1563. }
  1564. return;
  1565. }
  1566. const infoContainer = createInfoContainer(itemName, itemId);
  1567. sellerList.parentNode.insertBefore(infoContainer, sellerList);
  1568. sellerList.setAttribute('data-has-bazaar-container', 'true');
  1569. updateInfoContainer(infoContainer, itemId, itemName);
  1570. }
  1571.  
  1572. function processAllSellerWrappers(root = document.body) {
  1573. if (checkMobileView()) return;
  1574. const sellerWrappers = root.querySelectorAll('[class*="sellerListWrapper"]');
  1575. sellerWrappers.forEach(wrapper => processSellerWrapper(wrapper));
  1576. }
  1577. processAllSellerWrappers();
  1578. processMobileSellerList();
  1579.  
  1580. const observeTarget = document.querySelector('#root') || document.body;
  1581. let isProcessing = false;
  1582. const observer = new MutationObserver(mutations => {
  1583. if (isProcessing) return;
  1584. let needsProcessing = false;
  1585. mutations.forEach(mutation => {
  1586. const isOurMutation = Array.from(mutation.addedNodes).some(node =>
  1587. node.nodeType === Node.ELEMENT_NODE &&
  1588. (node.classList.contains('bazaar-info-container') ||
  1589. node.querySelector('.bazaar-info-container'))
  1590. );
  1591. if (isOurMutation) return;
  1592. mutation.addedNodes.forEach(node => {
  1593. if (node.nodeType === Node.ELEMENT_NODE) {
  1594. needsProcessing = true;
  1595. }
  1596. });
  1597. mutation.removedNodes.forEach(node => {
  1598. if (node.nodeType === Node.ELEMENT_NODE &&
  1599. (node.matches('ul.sellerList___e4C9_') || node.matches('ul[class*="sellerList"]')) &&
  1600. checkMobileView()) {
  1601. const container = document.querySelector('.bazaar-info-container');
  1602. if (container) container.remove();
  1603. }
  1604. });
  1605. });
  1606. if (needsProcessing) {
  1607. if (observer.processingTimeout) {
  1608. clearTimeout(observer.processingTimeout);
  1609. }
  1610. observer.processingTimeout = setTimeout(() => {
  1611. try {
  1612. isProcessing = true;
  1613. if (checkMobileView()) {
  1614. processMobileSellerList();
  1615. } else {
  1616. processAllSellerWrappers();
  1617. }
  1618. } finally {
  1619. isProcessing = false;
  1620. observer.processingTimeout = null;
  1621. }
  1622. }, 100);
  1623. }
  1624. });
  1625. observer.observe(observeTarget, { childList: true, subtree: true });
  1626. const bodyObserver = new MutationObserver(mutations => {
  1627. mutations.forEach(mutation => {
  1628. if (mutation.attributeName === 'class') {
  1629. currentDarkMode = document.body.classList.contains('dark-mode');
  1630. }
  1631. });
  1632. });
  1633. bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
  1634.  
  1635. if (window.location.href.includes("bazaar.php")) {
  1636. function scrollToTargetItem() {
  1637. const params = new URLSearchParams(window.location.search);
  1638. const targetItemId = params.get("itemId"), highlight = params.get("highlight");
  1639. if (!targetItemId || highlight !== "1") return;
  1640. function removeHighlightParam() {
  1641. params.delete("highlight");
  1642. history.replaceState({}, "", window.location.pathname + "?" + params.toString() + window.location.hash);
  1643. }
  1644. function showToast(message) {
  1645. const toast = document.createElement('div');
  1646. toast.textContent = message;
  1647. toast.style.cssText = 'position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background-color:rgba(0,0,0,0.7); color:white; padding:10px 20px; border-radius:5px; z-index:100000; font-size:14px;';
  1648. document.body.appendChild(toast);
  1649. setTimeout(() => {
  1650. toast.remove();
  1651. }, 3000);
  1652. }
  1653. function findItemCard() {
  1654. const img = document.querySelector(`img[src*="/images/items/${targetItemId}/"]`);
  1655. return img ? img.closest('.item___GYCYJ') : null;
  1656. }
  1657. const scrollInterval = setInterval(() => {
  1658. const card = findItemCard();
  1659. if (card) {
  1660. clearInterval(scrollInterval);
  1661. removeHighlightParam();
  1662. card.classList.add("green-outline", "pop-flash");
  1663. card.scrollIntoView({ behavior: "smooth", block: "center" });
  1664. setTimeout(() => {
  1665. card.classList.remove("pop-flash");
  1666. }, 800);
  1667. } else {
  1668. if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
  1669. showToast("Item not found on this page.");
  1670. removeHighlightParam();
  1671. clearInterval(scrollInterval);
  1672. } else {
  1673. window.scrollBy({ top: 300, behavior: 'auto' });
  1674. }
  1675. }
  1676. }, 50);
  1677. }
  1678. function waitForItems() {
  1679. const container = document.querySelector('.ReactVirtualized__Grid__innerScrollContainer');
  1680. if (container && container.childElementCount > 0) {
  1681. scrollToTargetItem();
  1682. } else {
  1683. setTimeout(waitForItems, 500);
  1684. }
  1685. }
  1686. waitForItems();
  1687. }
  1688.  
  1689. function dailyCleanup() {
  1690. const lastCleanup = GM_getValue("lastDailyCleanup"),
  1691. oneDay = 24 * 60 * 60 * 1000,
  1692. now = Date.now();
  1693. if (!lastCleanup || (now - parseInt(lastCleanup, 10)) > oneDay) {
  1694. const sevenDays = 7 * 24 * 60 * 60 * 1000;
  1695.  
  1696. let keys = [];
  1697. try {
  1698. if (typeof GM_listValues === 'function') {
  1699. keys = GM_listValues();
  1700. }
  1701. if (keys.length === 0) {
  1702. const checkKey = (prefix) => {
  1703. let i = 0;
  1704. while (true) {
  1705. const testKey = `${prefix}${i}`;
  1706. const value = GM_getValue(testKey);
  1707. if (value === undefined) break;
  1708. keys.push(testKey);
  1709. i++;
  1710. }
  1711. };
  1712.  
  1713. ['visited_', 'tornBazaarCache_'].forEach(prefix => {
  1714. for (let id = 1; id <= 1000; id++) {
  1715. const key = `${prefix}${id}`;
  1716. const value = GM_getValue(key);
  1717. if (value !== undefined) {
  1718. keys.push(key);
  1719. }
  1720. }
  1721. });
  1722. }
  1723. } catch (e) {
  1724. console.error("Error listing storage keys:", e);
  1725. }
  1726.  
  1727. keys.forEach(key => {
  1728. if (key && (key.startsWith("visited_") || key.startsWith("tornBazaarCache_"))) {
  1729. try {
  1730. const val = JSON.parse(GM_getValue(key));
  1731. let ts = null;
  1732. if (key.startsWith("visited_") && val && val.lastClickedUpdated) {
  1733. ts = val.lastClickedUpdated;
  1734. } else if (key.startsWith("tornBazaarCache_") && val && val.timestamp) {
  1735. ts = val.timestamp;
  1736. } else {
  1737. GM_deleteValue(key);
  1738. }
  1739. if (ts !== null && (now - ts) > sevenDays) {
  1740. GM_deleteValue(key);
  1741. }
  1742. } catch (e) {
  1743. GM_deleteValue(key);
  1744. }
  1745. }
  1746. });
  1747.  
  1748. GM_setValue("lastDailyCleanup", now.toString());
  1749. }
  1750. }
  1751. dailyCleanup();
  1752.  
  1753. document.body.addEventListener('click', event => {
  1754. const container = event.target.closest('.bazaar-info-container');
  1755. if (!container) return;
  1756. if (event.target.matches('.bazaar-order-toggle')) {
  1757. currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc";
  1758. event.target.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
  1759. performSort(container);
  1760. }
  1761. if (event.target.matches('.bazaar-display-toggle')) {
  1762. displayMode = displayMode === "percentage" ? "profit" : "percentage";
  1763. event.target.textContent = displayMode === "percentage" ? "%" : "$";
  1764. scriptSettings.defaultDisplayMode = displayMode;
  1765. saveSettings();
  1766.  
  1767. const allContainers = document.querySelectorAll('.bazaar-info-container');
  1768. allContainers.forEach(container => {
  1769. renderVirtualCards(container);
  1770.  
  1771. const cardContainer = container.querySelector('.bazaar-card-container');
  1772. if (cardContainer) {
  1773. const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
  1774. const currentScroll = scrollWrapper ? scrollWrapper.scrollLeft : 0;
  1775.  
  1776. const itemId = container.dataset.itemid;
  1777. if (itemId) {
  1778. if (allListings && allListings.length > 0) {
  1779. cardContainer.innerHTML = '';
  1780. renderVirtualCards(container);
  1781.  
  1782. if (scrollWrapper) {
  1783. scrollWrapper.scrollLeft = currentScroll;
  1784. }
  1785. }
  1786. }
  1787. }
  1788. });
  1789.  
  1790. return;
  1791. }
  1792. });
  1793.  
  1794. document.body.addEventListener('input', event => {
  1795. const container = event.target.closest('.bazaar-info-container');
  1796. if (!container) return;
  1797. if (event.target.matches('.bazaar-min-qty')) {
  1798. clearTimeout(event.target.debounceTimer);
  1799. event.target.debounceTimer = setTimeout(() => {
  1800. const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
  1801. if (scrollWrapper) {
  1802. scrollWrapper.scrollLeft = 0;
  1803. }
  1804. container.lastRenderScrollLeft = undefined;
  1805. if (!allListings || allListings.length === 0) {
  1806. const itemId = container.getAttribute('data-itemid');
  1807. if (itemId) {
  1808. const cachedData = getCache(itemId);
  1809. if (cachedData && cachedData.listings && cachedData.listings.length > 0) {
  1810. allListings = sortListings(cachedData.listings);
  1811. }
  1812. }
  1813. }
  1814. renderVirtualCards(container);
  1815. }, 300);
  1816. }
  1817. });
  1818.  
  1819. document.body.addEventListener('change', event => {
  1820. const container = event.target.closest('.bazaar-info-container');
  1821. if (!container) return;
  1822. if (event.target.matches('.bazaar-sort-select')) {
  1823. const newSortKey = event.target.value;
  1824. if (newSortKey !== currentSortKey) {
  1825. currentSortKey = newSortKey;
  1826. currentSortOrder = getSortOrderForKey(currentSortKey);
  1827. const orderToggle = container.querySelector('.bazaar-order-toggle');
  1828. if (orderToggle) {
  1829. orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
  1830. }
  1831. } else {
  1832. currentSortKey = newSortKey;
  1833. }
  1834. performSort(container);
  1835. }
  1836. });
  1837.  
  1838. function performSort(container) {
  1839. allListings = sortListings(allListings);
  1840. const cardContainer = container.querySelector('.bazaar-card-container');
  1841. const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
  1842. if (cardContainer && scrollWrapper) {
  1843. scrollWrapper.scrollLeft = 0;
  1844. container.lastRenderScrollLeft = undefined;
  1845. renderVirtualCards(container);
  1846. }
  1847. }
  1848.  
  1849. function addSettingsMenuItem() {
  1850. const menu = document.querySelector('.settings-menu');
  1851. if (!menu || document.querySelector('.bazaar-settings-button')) return;
  1852. const li = document.createElement('li');
  1853. li.className = 'link bazaar-settings-button';
  1854. const a = document.createElement('a');
  1855. a.href = '#';
  1856. const iconDiv = document.createElement('div');
  1857. iconDiv.className = 'icon-wrapper';
  1858. const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  1859. svgIcon.setAttribute('class', 'default');
  1860. svgIcon.setAttribute('fill', '#fff');
  1861. svgIcon.setAttribute('stroke', 'transparent');
  1862. svgIcon.setAttribute('stroke-width', '0');
  1863. svgIcon.setAttribute('width', '16');
  1864. svgIcon.setAttribute('height', '16');
  1865. svgIcon.setAttribute('viewBox', '0 0 640 512');
  1866. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  1867. path.setAttribute('d', 'M36.8 192l566.3 0c20.3 0 36.8-16.5 36.8-36.8c0-7.3-2.2-14.4-6.2-20.4L558.2 21.4C549.3 8 534.4 0 518.3 0L121.7 0c-16 0-31 8-39.9 21.4L6.2 134.7c-4 6.1-6.2 13.2-6.2 20.4C0 175.5 16.5 192 36.8 192zM64 224l0 160 0 80c0 26.5 21.5 48 48 48l224 0c26.5 0 48-21.5 48-48l0-80 0-160-64 0 0 160-192 0 0-160-64 0zm448 0l0 256c0 17.7 14.3 32 32 32s32-14.3 32-32l0-256-64 0z');
  1868. const span = document.createElement('span');
  1869. span.textContent = 'Bazaar Settings';
  1870. svgIcon.appendChild(path);
  1871. iconDiv.appendChild(svgIcon);
  1872. a.appendChild(iconDiv);
  1873. a.appendChild(span);
  1874. li.appendChild(a);
  1875. a.addEventListener('click', e => {
  1876. e.preventDefault();
  1877. document.body.click();
  1878. openSettingsModal();
  1879. });
  1880. const logoutButton = menu.querySelector('li.logout');
  1881. if (logoutButton) {
  1882. menu.insertBefore(li, logoutButton);
  1883. } else {
  1884. menu.appendChild(li);
  1885. }
  1886. }
  1887.  
  1888. function openSettingsModal() {
  1889. const overlay = document.createElement("div");
  1890. overlay.className = "bazaar-modal-overlay";
  1891. const modal = document.createElement("div");
  1892. modal.className = "bazaar-settings-modal";
  1893. modal.innerHTML = `
  1894. <div class="bazaar-settings-title">Bazaar Listings Settings</div>
  1895. <div class="bazaar-tabs">
  1896. <div class="bazaar-tab active" data-tab="settings">Settings</div>
  1897. <div class="bazaar-tab" data-tab="scripts">Other Scripts</div>
  1898. </div>
  1899. <div class="bazaar-tab-content active" id="tab-settings" style="max-height: 350px; overflow-y: auto;">
  1900. <div class="bazaar-settings-group">
  1901. <div class="bazaar-settings-item">
  1902. <label for="bazaar-api-key">Torn API Key (Optional)</label>
  1903. <div style="display: flex; gap: 5px; align-items: center; width: 100%;">
  1904. <input type="text" id="bazaar-api-key" value="${scriptSettings.apiKey || ''}" placeholder="Enter your API key here" style="flex-grow: 1; max-width: none;">
  1905. <button class="bazaar-button refresh-market-data" id="refresh-market-data" style="white-space: nowrap; padding: 8px 10px; height: 35px;">Refresh Values</button>
  1906. </div>
  1907. <div id="refresh-status" style="margin-top: 5px; font-size: 12px; display: none;"></div>
  1908. <div class="bazaar-api-note">
  1909. Providing an API key enables market value comparison. Your key stays local.<br>
  1910. Alternatively, install <a href="https://gf.qytechs.cn/en/scripts/527925-customizable-bazaar-filler" target="_blank">Bazaar Filler</a>, which works seamlessly with this script (Only ONE API call is made each day!)
  1911. </div>
  1912. </div>
  1913. <div class="bazaar-settings-item">
  1914. <label for="bazaar-default-sort">Default Sort</label>
  1915. <select id="bazaar-default-sort">
  1916. <option value="price" ${scriptSettings.defaultSort === 'price' ? 'selected' : ''}>Price</option>
  1917. <option value="quantity" ${scriptSettings.defaultSort === 'quantity' ? 'selected' : ''}>Quantity</option>
  1918. <option value="profit" ${scriptSettings.defaultSort === 'profit' ? 'selected' : ''}>Profit</option>
  1919. <option value="updated" ${scriptSettings.defaultSort === 'updated' ? 'selected' : ''}>Last Updated</option>
  1920. </select>
  1921. <div class="bazaar-api-note">
  1922. Choose how listings are sorted: Price, Quantity, Profit, or Last Updated.
  1923. </div>
  1924. </div>
  1925. <div class="bazaar-settings-item">
  1926. <label for="bazaar-default-order">Default Order</label>
  1927. <select id="bazaar-default-order">
  1928. <option value="asc" ${scriptSettings.defaultOrder === 'asc' ? 'selected' : ''}>Ascending</option>
  1929. <option value="desc" ${scriptSettings.defaultOrder === 'desc' ? 'selected' : ''}>Descending</option>
  1930. </select>
  1931. <div class="bazaar-api-note">
  1932. Choose the sorting direction.
  1933. </div>
  1934. </div>
  1935. <div class="bazaar-settings-item">
  1936. <label for="bazaar-listing-fee">Listing Fee (%)</label>
  1937. <input type="number" id="bazaar-listing-fee" class="bazaar-number-input" value="${scriptSettings.listingFee || 0}" min="0" max="100" step="1">
  1938. <div class="bazaar-api-note">
  1939. Set the fee percentage when listing items. (e.g., 10% fee means $10,000 on $100,000)
  1940. </div>
  1941. </div>
  1942. <div class="bazaar-settings-item">
  1943. <label for="bazaar-default-display">Default Display Mode</label>
  1944. <select id="bazaar-default-display">
  1945. <option value="percentage" ${scriptSettings.defaultDisplayMode === 'percentage' ? 'selected' : ''}>Percentage Difference</option>
  1946. <option value="profit" ${scriptSettings.defaultDisplayMode === 'profit' ? 'selected' : ''}>Potential Profit</option>
  1947. </select>
  1948. <div class="bazaar-api-note">
  1949. Choose whether to display price comparisons as a percentage or in dollars.
  1950. </div>
  1951. </div>
  1952. <div class="bazaar-settings-item">
  1953. <label for="bazaar-link-behavior">Bazaar Link Click Behavior</label>
  1954. <select id="bazaar-link-behavior">
  1955. <option value="new_tab" ${scriptSettings.linkBehavior === 'new_tab' ? 'selected' : ''}>Open in New Tab</option>
  1956. <option value="new_window" ${scriptSettings.linkBehavior === 'new_window' ? 'selected' : ''}>Open in New Window</option>
  1957. <option value="same_tab" ${scriptSettings.linkBehavior === 'same_tab' ? 'selected' : ''}>Open in Same Tab</option>
  1958. </select>
  1959. <div class="bazaar-api-note">
  1960. Choose how bazaar links open when clicked.
  1961. </div>
  1962. </div>
  1963. </div>
  1964. </div>
  1965. <div class="bazaar-tab-content" id="tab-scripts" style="max-height: 350px; overflow-y: auto;">
  1966. <div class="bazaar-script-item">
  1967. <div class="bazaar-script-name">Customizable Bazaar Filler</div>
  1968. <div class="bazaar-script-desc">Auto-fills bazaar item quantities and prices.</div>
  1969. <a href="https://gf.qytechs.cn/en/scripts/527925-customizable-bazaar-filler" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1970. </div>
  1971. <div class="bazaar-script-item">
  1972. <div class="bazaar-script-name">Torn Item Market Highlighter</div>
  1973. <div class="bazaar-script-desc">Highlights items based on rules and prices.</div>
  1974. <a href="https://gf.qytechs.cn/en/scripts/513617-torn-item-market-highlighter" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1975. </div>
  1976. <div class="bazaar-script-item">
  1977. <div class="bazaar-script-name">Torn Item Market Max Quantity Calculator</div>
  1978. <div class="bazaar-script-desc">Calculates the max quantity you can buy.</div>
  1979. <a href="https://gf.qytechs.cn/en/scripts/513790-torn-item-market-max-quantity-calculator" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1980. </div>
  1981. <div class="bazaar-script-item">
  1982. <div class="bazaar-script-name">Enhanced Chat Buttons V2</div>
  1983. <div class="bazaar-script-desc">Improves chat with extra buttons.</div>
  1984. <a href="https://gf.qytechs.cn/en/scripts/488294-torn-com-enhanced-chat-buttons-v2" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1985. </div>
  1986. <div class="bazaar-script-item">
  1987. <div class="bazaar-script-name">Market Item Locker</div>
  1988. <div class="bazaar-script-desc">Lock items when listing to avoid accidental sales.</div>
  1989. <a href="https://gf.qytechs.cn/en/scripts/513784-torn-market-item-locker" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1990. </div>
  1991. <div class="bazaar-script-item">
  1992. <div class="bazaar-script-name">Market Quick Remove</div>
  1993. <div class="bazaar-script-desc">Quickly remove items from your listings.</div>
  1994. <a href="https://gf.qytechs.cn/en/scripts/515870-torn-market-quick-remove" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  1995. </div>
  1996. <div class="bazaar-script-item">
  1997. <div class="bazaar-script-name">Trade Chat Timer on Button</div>
  1998. <div class="bazaar-script-desc">Adds a timer to the trade chat button.</div>
  1999. <a href="https://gf.qytechs.cn/en/scripts/496284-trade-chat-timer-on-button" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
  2000. </div>
  2001. </div>
  2002. <div class="bazaar-settings-buttons">
  2003. <button class="bazaar-settings-save">Save</button>
  2004. <button class="bazaar-settings-cancel">Cancel</button>
  2005. </div>
  2006. <div class="bazaar-settings-footer">
  2007. <p>This script uses data from <a href="https://tornpal.com" target="_blank">TornPal</a> and <a href="https://www.ironnerd.me/torn/" target="_blank">IronNerd</a>.</p>
  2008. <p>Created by <a href="https://www.torn.com/profiles.php?XID=1853324" target="_blank">Weav3r [1853324]</a></p>
  2009. </div>
  2010. `;
  2011. overlay.appendChild(modal);
  2012. const tabs = modal.querySelectorAll('.bazaar-tab');
  2013. tabs.forEach(tab => {
  2014. tab.addEventListener('click', function () {
  2015. tabs.forEach(t => t.classList.remove('active'));
  2016. this.classList.add('active');
  2017. modal.querySelectorAll('.bazaar-tab-content').forEach(content => content.classList.remove('active'));
  2018. document.getElementById(`tab-${this.getAttribute('data-tab')}`).classList.add('active');
  2019. });
  2020. });
  2021. modal.querySelector('.bazaar-settings-save').addEventListener('click', () => {
  2022. saveSettingsFromModal(modal);
  2023. overlay.remove();
  2024. });
  2025. modal.querySelector('.bazaar-settings-cancel').addEventListener('click', () => {
  2026. overlay.remove();
  2027. });
  2028. overlay.addEventListener('click', e => {
  2029. if (e.target === overlay) overlay.remove();
  2030. });
  2031. document.body.appendChild(overlay);
  2032. }
  2033.  
  2034. function saveSettingsFromModal(modal) {
  2035. const oldLinkBehavior = scriptSettings.linkBehavior;
  2036. scriptSettings.apiKey = modal.querySelector('#bazaar-api-key').value.trim();
  2037. scriptSettings.defaultSort = modal.querySelector('#bazaar-default-sort').value;
  2038. scriptSettings.defaultOrder = modal.querySelector('#bazaar-default-order').value;
  2039. scriptSettings.listingFee = Math.round(parseFloat(modal.querySelector('#bazaar-listing-fee').value) || 0);
  2040. scriptSettings.defaultDisplayMode = modal.querySelector('#bazaar-default-display').value;
  2041. scriptSettings.linkBehavior = modal.querySelector('#bazaar-link-behavior').value;
  2042. if (scriptSettings.listingFee < 0) scriptSettings.listingFee = 0;
  2043. if (scriptSettings.listingFee > 100) scriptSettings.listingFee = 100;
  2044. currentSortKey = scriptSettings.defaultSort;
  2045. currentSortOrder = scriptSettings.defaultOrder;
  2046. displayMode = scriptSettings.defaultDisplayMode;
  2047. saveSettings();
  2048. document.querySelectorAll('.bazaar-info-container').forEach(container => {
  2049. const sortSelect = container.querySelector('.bazaar-sort-select');
  2050. if (sortSelect) sortSelect.value = currentSortKey;
  2051. const orderToggle = container.querySelector('.bazaar-order-toggle');
  2052. if (orderToggle) orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
  2053. const displayToggle = container.querySelector('.bazaar-display-toggle');
  2054. if (displayToggle) displayToggle.textContent = displayMode === "percentage" ? "%" : "$";
  2055. if (oldLinkBehavior !== scriptSettings.linkBehavior) {
  2056. const cardContainer = container.querySelector('.bazaar-card-container');
  2057. if (cardContainer) {
  2058. cardContainer.innerHTML = '';
  2059. container.lastRenderScrollLeft = undefined;
  2060. renderVirtualCards(container);
  2061. }
  2062. } else {
  2063. performSort(container);
  2064. }
  2065. });
  2066. if (scriptSettings.apiKey) {
  2067. fetchTornItems(true);
  2068. }
  2069. }
  2070.  
  2071. function fetchTornItems(forceRefresh = false) {
  2072. const stored = GM_getValue("tornItems"),
  2073. lastUpdated = GM_getValue("lastTornItemsUpdate") || 0,
  2074. now = Date.now(),
  2075. oneDayMs = 24 * 60 * 60 * 1000,
  2076. lastUTC = new Date(parseInt(lastUpdated)).toISOString().split('T')[0],
  2077. todayUTC = new Date().toISOString().split('T')[0],
  2078. lastHour = Math.floor(parseInt(lastUpdated) / (60 * 60 * 1000)),
  2079. currentHour = Math.floor(now / (60 * 60 * 1000));
  2080.  
  2081. const needsRefresh = forceRefresh ||
  2082. lastUTC < todayUTC ||
  2083. (now - lastUpdated) >= oneDayMs ||
  2084. (lastHour < currentHour && (currentHour - lastHour) >= 1);
  2085. if (scriptSettings.apiKey && (!stored || needsRefresh)) {
  2086. const refreshStatus = document.getElementById('refresh-status');
  2087. if (refreshStatus) {
  2088. refreshStatus.style.display = 'block';
  2089. refreshStatus.textContent = 'Fetching market values...';
  2090. refreshStatus.style.color = currentDarkMode ? '#aaa' : '#666';
  2091. }
  2092. return fetch(`https://api.torn.com/torn/?key=${scriptSettings.apiKey}&selections=items&comment=wBazaars`)
  2093. .then(r => r.json())
  2094. .then(data => {
  2095. if (!data.items) {
  2096. console.error("Failed to fetch Torn items. Check your API key or rate limit.");
  2097. if (refreshStatus) {
  2098. refreshStatus.textContent = data.error ? `Error: ${data.error.error}` : 'Failed to fetch market values. Check your API key.';
  2099. refreshStatus.style.color = '#cc0000';
  2100. setTimeout(() => {
  2101. refreshStatus.style.display = 'none';
  2102. }, 5000);
  2103. }
  2104. return false;
  2105. }
  2106. cachedItemsData = null;
  2107. const filtered = {};
  2108. for (let [id, item] of Object.entries(data.items)) {
  2109. if (item.tradeable) {
  2110. filtered[id] = { name: item.name, market_value: item.market_value };
  2111. }
  2112. }
  2113. GM_setValue("tornItems", JSON.stringify(filtered));
  2114. GM_setValue("lastTornItemsUpdate", now.toString());
  2115. if (refreshStatus) {
  2116. refreshStatus.textContent = `Market values updated successfully! (${todayUTC})`;
  2117. refreshStatus.style.color = '#009900';
  2118. setTimeout(() => {
  2119. refreshStatus.style.display = 'none';
  2120. }, 3000);
  2121. }
  2122. document.querySelectorAll('.bazaar-info-container').forEach(container => {
  2123. if (container.isConnected) {
  2124. const cardContainer = container.querySelector('.bazaar-card-container');
  2125. if (cardContainer) {
  2126. cardContainer.innerHTML = '';
  2127. container.lastRenderScrollLeft = undefined;
  2128. renderVirtualCards(container);
  2129. }
  2130. }
  2131. });
  2132. return true;
  2133. })
  2134. .catch(err => {
  2135. console.error("Error fetching Torn items:", err);
  2136. if (refreshStatus) {
  2137. refreshStatus.textContent = `Error: ${err.message || 'Failed to fetch market values'}`;
  2138. refreshStatus.style.color = '#cc0000';
  2139. setTimeout(() => {
  2140. refreshStatus.style.display = 'none';
  2141. }, 5000);
  2142. }
  2143. return false;
  2144. });
  2145. }
  2146. return Promise.resolve(false);
  2147. }
  2148.  
  2149. document.body.addEventListener('click', event => {
  2150. if (event.target.id === 'refresh-market-data' || event.target.closest('#refresh-market-data')) {
  2151. event.preventDefault();
  2152. const apiKeyInput = document.getElementById('bazaar-api-key');
  2153. const refreshStatus = document.getElementById('refresh-status');
  2154. if (!apiKeyInput || !apiKeyInput.value.trim()) {
  2155. if (refreshStatus) {
  2156. refreshStatus.style.display = 'block';
  2157. refreshStatus.textContent = 'Please enter an API key first.';
  2158. refreshStatus.style.color = '#cc0000';
  2159. setTimeout(() => {
  2160. refreshStatus.style.display = 'none';
  2161. }, 3000);
  2162. }
  2163. return;
  2164. }
  2165. scriptSettings.apiKey = apiKeyInput.value.trim();
  2166. fetchTornItems(true);
  2167. }
  2168. });
  2169.  
  2170. function observeUserMenu() {
  2171. const menuObserver = new MutationObserver(mutations => {
  2172. mutations.forEach(mutation => {
  2173. if (mutation.addedNodes.length > 0) {
  2174. for (const node of mutation.addedNodes) {
  2175. if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('settings-menu')) {
  2176. addSettingsMenuItem();
  2177. break;
  2178. }
  2179. }
  2180. }
  2181. });
  2182. });
  2183. menuObserver.observe(document.body, { childList: true, subtree: true });
  2184. if (document.querySelector('.settings-menu')) {
  2185. addSettingsMenuItem();
  2186. }
  2187. }
  2188. observeUserMenu();
  2189.  
  2190. function getSortOrderForKey(key) {
  2191. return key === "price" ? "asc" : "desc";
  2192. }
  2193.  
  2194. function cleanupResources() {
  2195. if (observer) {
  2196. observer.disconnect();
  2197. }
  2198. if (bodyObserver) {
  2199. bodyObserver.disconnect();
  2200. }
  2201. document.querySelectorAll('.bazaar-scroll-container').forEach(container => {
  2202. const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
  2203. if (scrollWrapper && scrollWrapper.isScrolling) {
  2204. cancelAnimationFrame(scrollWrapper.scrollAnimationId);
  2205. }
  2206. });
  2207. }
  2208. window.addEventListener('beforeunload', cleanupResources);
  2209. })();
  2210.  

QingJ © 2025

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