// ==UserScript==
// @name Bazaars in Item Market Powered by TornW3B
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Displays bazaar listings with sorting controls
// @author Weav3r
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://www.torn.com/bazaar.php*
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect weav3r.dev
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @run-at document-end
// ==/UserScript==
(function($) {
'use strict';
const PDA = {
isDetected: false,
handlers: {},
async detect() {
try {
if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
const response = await window.flutter_inappwebview.callHandler('isTornPDA');
this.isDetected = response && response.isTornPDA;
if (this.isDetected) {
this.setupHandlers();
}
} else if (navigator.userAgent && navigator.userAgent.includes('TornPDA')) {
this.isDetected = true;
this.setupHandlers();
}
} catch (e) {
this.isDetected = false;
}
return this.isDetected;
},
setupHandlers() {
this.handlers.httpGet = async (url, headers = {}) => {
try {
const response = await window.flutter_inappwebview.callHandler('PDA_httpGet', url, headers);
return {
responseText: response.responseText,
status: response.status,
statusText: response.statusText,
responseHeaders: response.responseHeaders
};
} catch (error) {
throw new Error(`PDA HTTP GET failed: ${error}`);
}
};
this.handlers.httpPost = async (url, headers = {}, body = '') => {
try {
const response = await window.flutter_inappwebview.callHandler('PDA_httpPost', url, headers, body);
return {
responseText: response.responseText,
status: response.status,
statusText: response.statusText,
responseHeaders: response.responseHeaders
};
} catch (error) {
throw new Error(`PDA HTTP POST failed: ${error}`);
}
};
this.handlers.evaluate = async (code) => {
try {
await window.flutter_inappwebview.callHandler('PDA_evaluateJavascript', code);
} catch (error) {
throw new Error(`PDA JS evaluation failed: ${error}`);
}
};
},
async httpRequest(options) {
if (this.isDetected && options.method === 'GET') {
const response = await this.handlers.httpGet(options.url, options.headers || {});
return {
responseText: response.responseText,
status: response.status,
readyState: 4,
response: response.responseText
};
} else if (this.isDetected && options.method === 'POST') {
const response = await this.handlers.httpPost(options.url, options.headers || {}, options.data || '');
return {
responseText: response.responseText,
status: response.status,
readyState: 4,
response: response.responseText
};
} else {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: resolve,
onerror: reject
});
});
}
}
};
PDA.detect();
const CONFIG = {
CACHE_DURATION: 30000,
ITEMS_PER_PAGE: 3,
DEFAULT_SORT: 'price-asc',
DEFAULT_MIN_QTY: 0,
SHOW_MARKET_COMPARISON: true,
BAZAAR_CLICK_BEHAVIOR: 'new-tab',
SELECTORS: {
DESKTOP_LIST: '.sellerListWrapper___PN32N .sellerList___kgAh_',
MOBILE_LIST: '.sellerList___e4C9_',
MOBILE_HEADER: '.itemsHeader___ZTO9r',
MOBILE_ROW: '.rowWrapper___OrFGK'
},
MOBILE_BREAKPOINT: 784
};
const loadSettings = () => {
const saved = localStorage.getItem('bazaarListingsSettings');
if (saved) {
try {
const settings = JSON.parse(saved);
return { ...CONFIG, ...settings };
} catch (e) {
}
}
return CONFIG;
};
const saveSettings = (settings) => {
localStorage.setItem('bazaarListingsSettings', JSON.stringify(settings));
Object.assign(CONFIG, settings);
};
Object.assign(CONFIG, loadSettings());
const state = {
originalFetch: null,
currentDisplay: null,
apiCache: new Map(),
currentItemID: null
};
const injectStyles = () => {
const styles = `
:root {
--base-font-size: clamp(12px, 1vw, 14px);
--small-font-size: clamp(11px, 0.9vw, 12px);
--tiny-font-size: clamp(10px, 0.8vw, 11px);
--large-font-size: clamp(13px, 1.1vw, 15px);
--title-font-size: clamp(14px, 1.2vw, 16px);
--spacing-xs: clamp(3px, 0.3vw, 3px);
--spacing-sm: clamp(5px, 0.5vw, 6px);
--spacing-md: clamp(8px, 0.8vw, 10px);
--spacing-lg: clamp(12px, 1.2vw, 16px);
--bazaar-bg: #f5f5f5;
--bazaar-card-bg: white;
--bazaar-card-border: #ddd;
--bazaar-card-shadow: rgba(0,0,0,0.06);
--bazaar-card-shadow-hover: rgba(0,0,0,0.10);
--bazaar-text-primary: #222;
--bazaar-text-secondary: #666;
--bazaar-text-tertiary: #888;
--bazaar-price-color: #0070e0;
--bazaar-nav-bg: #333;
--bazaar-nav-bg-hover: #555;
--bazaar-nav-disabled: #ccc;
--bazaar-border-light: #eee;
--bazaar-input-border: #ddd;
--bazaar-error-color: #cc0000;
--bazaar-qty-color: #444;
--bazaar-container-border: #d0d0d0;
--bazaar-container-bg: rgba(248, 249, 250, 0.6);
--bazaar-container-shadow: rgba(0,0,0,0.08);
}
body.dark-mode {
--bazaar-bg: #1a1a1a;
--bazaar-card-bg: #2a2a2a;
--bazaar-card-border: #444;
--bazaar-card-shadow: rgba(0,0,0,0.3);
--bazaar-card-shadow-hover: rgba(0,0,0,0.5);
--bazaar-text-primary: #e8e8e8;
--bazaar-text-secondary: #aaa;
--bazaar-text-tertiary: #888;
--bazaar-price-color: #4da6ff;
--bazaar-nav-bg: #444;
--bazaar-nav-bg-hover: #666;
--bazaar-nav-disabled: #333;
--bazaar-border-light: #3a3a3a;
--bazaar-input-border: #555;
--bazaar-error-color: #ff6666;
--bazaar-qty-color: #ccc;
--bazaar-container-border: #555;
--bazaar-container-bg: rgba(35, 35, 35, 0.8);
--bazaar-container-shadow: rgba(0,0,0,0.4);
}
.bazaar-container {
display: block;
list-style: none;
margin-bottom: var(--spacing-md);
padding: clamp(12px, 1.5vw, 16px);
border: 2px solid var(--bazaar-container-border);
border-radius: clamp(6px, 1vw, 8px);
background: var(--bazaar-container-bg);
box-shadow: 0 2px 4px var(--bazaar-container-shadow);
}
.itemsHeader___ZTO9r + .bazaar-container {
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.bazaar-controls {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--bazaar-bg);
border-radius: clamp(3px, 0.5vw, 4px);
margin-bottom: var(--spacing-md);
align-items: center;
border: 1px solid var(--bazaar-border-light);
}
.bazaar-filter {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-right: var(--spacing-md);
}
.bazaar-filter label {
font-size: var(--small-font-size);
color: var(--bazaar-text-secondary);
}
.bazaar-filter input {
width: clamp(55px, 8vw, 65px);
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--bazaar-input-border);
border-radius: clamp(2px, 0.4vw, 3px);
font-size: var(--small-font-size);
background: var(--bazaar-card-bg);
color: var(--bazaar-text-primary);
}
.bazaar-sort-buttons {
display: flex;
gap: var(--spacing-xs);
align-items: center;
}
.bazaar-sort-label {
font-size: var(--small-font-size);
color: var(--bazaar-text-secondary);
margin-right: var(--spacing-xs);
}
.bazaar-sort-btn {
padding: var(--spacing-xs) var(--spacing-md);
border: 1px solid var(--bazaar-input-border);
background: var(--bazaar-card-bg);
color: var(--bazaar-text-primary);
border-radius: clamp(2px, 0.4vw, 3px);
font-size: var(--small-font-size);
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
display: flex;
align-items: center;
gap: var(--spacing-xs);
min-width: clamp(50px, 7vw, 55px);
}
.bazaar-sort-btn:hover {
background: var(--bazaar-bg);
}
.bazaar-sort-btn[data-state="asc"],
.bazaar-sort-btn[data-state="desc"] {
background: var(--bazaar-nav-bg);
color: white;
border-color: var(--bazaar-nav-bg);
}
.bazaar-sort-btn[data-state="asc"]:hover,
.bazaar-sort-btn[data-state="desc"]:hover {
background: var(--bazaar-nav-bg-hover);
border-color: var(--bazaar-nav-bg-hover);
}
.sort-arrow {
font-size: var(--tiny-font-size);
opacity: 0.8;
}
/* Touch indicator for mobile */
@media (pointer: coarse) {
.bazaar-display::after {
content: '';
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: var(--bazaar-nav-disabled);
border-radius: 2px;
opacity: 0.5;
transition: opacity 0.3s;
}
.bazaar-container:hover .bazaar-display::after {
opacity: 0.8;
}
}
@media (max-width: 784px) {
:root {
--base-font-size: clamp(12px, 2vw, 13px);
--small-font-size: clamp(11px, 1.7vw, 12px);
--tiny-font-size: clamp(10px, 1.5vw, 11px);
--large-font-size: clamp(13px, 2vw, 14px);
--title-font-size: clamp(14px, 2.2vw, 15px);
}
.bazaar-container {
margin: var(--spacing-sm) 0;
padding: max(var(--spacing-md), 12px);
}
.bazaar-controls {
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-sm);
}
.bazaar-filter {
width: auto;
max-width: fit-content;
justify-content: flex-start;
align-items: baseline;
gap: var(--spacing-xs);
margin-right: var(--spacing-md);
}
.bazaar-filter input {
margin-right: 0;
}
.bazaar-sort-buttons {
width: 100%;
justify-content: space-between;
gap: var(--spacing-xs);
}
.bazaar-sort-label {
display: none;
}
.bazaar-sort-btn {
flex: 1;
padding: var(--spacing-sm) var(--spacing-sm);
min-width: auto;
}
.bazaar-display {
margin-top: var(--spacing-sm);
}
.bazaar-nav {
width: clamp(24px, 4vw, 28px);
min-height: clamp(40px, 8vw, 50px);
font-size: clamp(14px, 3vw, 18px);
border-radius: clamp(5px, 1.2vw, 8px);
}
.bazaar-cards-wrapper {
gap: var(--spacing-xs);
}
.bazaar-cards-wrapper.multi-row {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-xs);
}
.bazaar-card {
padding: var(--spacing-sm) var(--spacing-sm);
}
.bazaar-card-info {
align-items: center;
margin-bottom: var(--spacing-xs);
}
.bazaar-card-price,
.bazaar-card-qty {
width: 100%;
text-align: center;
}
.bazaar-card-name {
text-align: center;
margin-bottom: var(--spacing-sm);
}
.bazaar-card-bottom {
padding-top: var(--spacing-xs);
}
.rowWrapper___OrFGK .bazaar-container {
height: auto !important;
opacity: 1 !important;
transform: none !important;
position: relative;
}
div.bazaar-container {
width: 100%;
box-sizing: border-box;
}
}
.bazaar-display {
display: grid;
grid-template-columns: clamp(26px, 3vw, 30px) 1fr clamp(26px, 3vw, 30px);
align-items: stretch;
width: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
background: none;
padding: var(--spacing-sm);
gap: var(--spacing-sm);
position: relative;
touch-action: pan-y;
}
.bazaar-nav {
grid-row: 1;
z-index: 2;
opacity: 0.9;
background: var(--bazaar-nav-bg);
color: white;
border: none;
border-radius: clamp(6px, 1vw, 10px);
width: 100%;
height: 100%;
min-height: clamp(50px, 6vw, 55px);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(14px, 2.5vw, 20px);
transition: background 0.2s;
padding: 0;
}
.bazaar-nav.prev { grid-column: 1; justify-self: stretch; }
.bazaar-nav.next { grid-column: 3; justify-self: stretch; }
.bazaar-nav:hover:not(:disabled) {
background: var(--bazaar-nav-bg-hover);
opacity: 1;
}
.bazaar-nav:disabled {
background: var(--bazaar-nav-disabled);
cursor: not-allowed;
opacity: 0.6;
}
.bazaar-pagination-info {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
font-size: var(--small-font-size);
color: var(--bazaar-text-secondary);
}
.bazaar-nav-first,
.bazaar-nav-last {
background: var(--bazaar-nav-bg);
color: white;
border: none;
border-radius: clamp(3px, 0.5vw, 4px);
padding: var(--spacing-xs) var(--spacing-md);
cursor: pointer;
font-size: var(--small-font-size);
transition: all 0.2s;
opacity: 0.9;
}
.bazaar-nav-first:hover:not(:disabled),
.bazaar-nav-last:hover:not(:disabled) {
background: var(--bazaar-nav-bg-hover);
opacity: 1;
}
.bazaar-nav-first:disabled,
.bazaar-nav-last:disabled {
background: var(--bazaar-nav-disabled);
cursor: not-allowed;
opacity: 0.6;
}
.bazaar-page-info {
font-weight: 500;
}
.current-page, .total-pages {
font-weight: bold;
color: var(--bazaar-text-primary);
}
.bazaar-cards-wrapper {
grid-column: 2;
display: flex;
flex-wrap: wrap;
width: 100%;
gap: var(--spacing-xs);
justify-content: center;
align-items: stretch;
box-sizing: border-box;
margin: 0;
padding: 0;
transition: transform 0.2s ease-out;
}
.bazaar-display.swiping .bazaar-cards-wrapper {
transition: none;
}
.bazaar-cards-wrapper.multi-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-sm);
}
.bazaar-cards-wrapper.multi-row .bazaar-card {
max-width: none;
width: 100%;
}
.bazaar-error, .bazaar-empty {
padding: var(--spacing-lg);
text-align: center;
font-size: var(--base-font-size);
}
.bazaar-error {
color: var(--bazaar-error-color);
}
.bazaar-empty {
color: var(--bazaar-text-secondary);
}
.bazaar-card {
flex: 1 1 0;
min-width: 0;
max-width: 100%;
margin: 0 1px;
background: var(--bazaar-card-bg);
border: 1px solid var(--bazaar-card-border);
border-radius: clamp(4px, 0.8vw, 5px);
padding: clamp(10px, 1vw, 12px) clamp(8px, 0.8vw, 10px);
box-shadow: 0 1px 2px var(--bazaar-card-shadow);
transition: transform 0.2s, box-shadow 0.2s;
cursor: default;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
word-break: break-word;
overflow-wrap: anywhere;
}
.bazaar-card:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px var(--bazaar-card-shadow-hover);
}
.bazaar-card > div { margin-bottom: var(--spacing-xs); }
.bazaar-card-name {
font-weight: bold;
font-size: var(--large-font-size);
line-height: 1.2;
font-family: sans-serif;
color: var(--bazaar-text-primary);
margin-bottom: var(--spacing-sm);
letter-spacing: 0.1px;
word-break: break-word;
overflow-wrap: anywhere;
min-width: 0;
}
.bazaar-settings-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
}
.bazaar-settings-modal.show {
display: flex;
}
.bazaar-settings-content {
background: var(--settings-bg);
border-radius: clamp(6px, 1vw, 8px);
max-width: min(600px, 90vw);
width: 90%;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
position: relative;
display: flex;
flex-direction: column;
}
:root {
--settings-bg: #ffffff;
--settings-header-bg: #f8f9fa;
--settings-border: #dee2e6;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-tab-active: #0066cc;
--settings-tab-hover: #f8f9fa;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-button-primary: #28a745;
--settings-button-secondary: #6c757d;
--settings-credits-bg: #f8f9fa;
--settings-link: #0066cc;
}
body.dark-mode {
--settings-bg: #2b2b2b;
--settings-header-bg: #1f1f1f;
--settings-border: #444444;
--settings-text: #e8e8e8;
--settings-text-secondary: #aaaaaa;
--settings-tab-active: #4da6ff;
--settings-tab-hover: #3a3a3a;
--settings-input-bg: #3a3a3a;
--settings-input-border: #555555;
--settings-button-primary: #28a745;
--settings-button-secondary: #6c757d;
--settings-credits-bg: #1f1f1f;
--settings-link: #4da6ff;
}
.bazaar-settings-header {
background: var(--settings-header-bg);
padding: clamp(12px, 2vw, 20px) clamp(16px, 2.5vw, 24px);
border-bottom: 1px solid var(--settings-border);
position: relative;
}
.bazaar-settings-title {
font-size: clamp(16px, 2vw, 20px);
font-weight: 600;
color: var(--settings-text);
margin: 0;
padding-right: 40px;
}
.bazaar-settings-body {
flex: 1;
overflow-y: auto;
padding: clamp(16px, 2vw, 24px);
}
.bazaar-settings-body::-webkit-scrollbar {
width: 8px;
}
.bazaar-settings-body::-webkit-scrollbar-track {
background: var(--settings-bg);
}
.bazaar-settings-body::-webkit-scrollbar-thumb {
background: var(--settings-border);
border-radius: 4px;
}
.bazaar-settings-body::-webkit-scrollbar-thumb:hover {
background: var(--settings-text-secondary);
}
.bazaar-settings-section {
margin-bottom: clamp(16px, 2vw, 24px);
}
.bazaar-settings-section-title {
font-size: clamp(14px, 1.5vw, 16px);
font-weight: 600;
color: var(--settings-text);
margin-bottom: clamp(12px, 1.5vw, 16px);
}
.bazaar-settings-field {
margin-bottom: clamp(12px, 1.5vw, 16px);
}
.bazaar-settings-field-label {
display: block;
font-size: clamp(12px, 1.2vw, 14px);
font-weight: 500;
color: var(--settings-text);
margin-bottom: 4px;
}
.bazaar-settings-field-description {
font-size: clamp(10px, 1vw, 12px);
color: var(--settings-text-secondary);
margin-bottom: 8px;
line-height: 1.4;
}
.bazaar-settings-input,
.bazaar-settings-select {
padding: clamp(6px, 0.8vw, 8px) clamp(8px, 1vw, 12px);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background: var(--settings-input-bg);
color: var(--settings-text);
font-size: clamp(12px, 1.2vw, 14px);
transition: all 0.2s;
}
.bazaar-settings-input[type="number"] {
width: 120px;
}
.bazaar-settings-select {
width: auto;
min-width: 200px;
max-width: 300px;
}
.bazaar-settings-input:focus,
.bazaar-settings-select:focus {
outline: none;
border-color: var(--settings-tab-active);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
body.dark-mode .bazaar-settings-input:focus,
body.dark-mode .bazaar-settings-select:focus {
box-shadow: 0 0 0 3px rgba(77, 166, 255, 0.2);
}
.bazaar-settings-toggle-field {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.bazaar-settings-toggle-info {
flex: 1;
margin-right: 16px;
}
.bazaar-toggle-switch {
position: relative;
width: 48px;
height: 24px;
background: var(--settings-input-border);
border-radius: 12px;
cursor: pointer;
transition: background 0.3s;
flex-shrink: 0;
}
.bazaar-toggle-switch.active {
background: var(--settings-button-primary);
}
.bazaar-toggle-slider {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.bazaar-toggle-switch.active .bazaar-toggle-slider {
transform: translateX(24px);
}
.bazaar-settings-actions {
display: flex;
gap: clamp(8px, 1vw, 12px);
justify-content: flex-end;
padding: clamp(12px, 1.5vw, 16px) clamp(16px, 2vw, 24px);
border-top: 1px solid var(--settings-border);
background: var(--settings-header-bg);
}
.bazaar-settings-btn {
padding: clamp(8px, 1vw, 10px) clamp(16px, 1.5vw, 20px);
border: none;
border-radius: 4px;
font-size: clamp(12px, 1.2vw, 14px);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.bazaar-settings-btn-primary {
background: var(--settings-button-primary);
color: white;
}
.bazaar-settings-btn-primary:hover {
background: #218838;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
}
.bazaar-settings-btn-secondary {
background: var(--settings-button-secondary);
color: white;
}
.bazaar-settings-btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3);
}
.bazaar-settings-credits {
background: var(--settings-credits-bg);
padding: clamp(12px, 1.5vw, 16px) clamp(16px, 2vw, 24px);
border-top: 1px solid var(--settings-border);
text-align: center;
font-size: clamp(10px, 1vw, 12px);
color: var(--settings-text-secondary);
line-height: 1.6;
}
.bazaar-settings-credits a {
color: var(--settings-link);
text-decoration: none;
font-weight: 500;
transition: opacity 0.2s;
}
.bazaar-settings-credits a:hover {
opacity: 0.8;
text-decoration: underline;
}
.bazaar-settings-credits-divider {
margin: 0 8px;
color: var(--settings-text-secondary);
}
.bazaar-settings-close {
position: absolute;
top: 20px;
right: 24px;
background: none;
border: none;
font-size: 24px;
color: var(--settings-text-secondary);
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.bazaar-settings-close:hover {
background: var(--settings-tab-hover);
color: var(--settings-text);
}
.bazaar-card-name a {
color: #0066cc;
text-decoration: none;
transition: opacity 0.2s;
}
.bazaar-card-name a:visited {
color: #800080 !important;
}
.bazaar-card-name a:hover {
opacity: 0.8;
text-decoration: underline;
}
body.dark-mode .bazaar-card-name a {
color: #4da6ff;
}
body.dark-mode .bazaar-card-name a:visited {
color: #b366ff !important;
}
.bazaar-card-info {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
min-width: 0;
word-break: break-word;
overflow-wrap: anywhere;
}
.bazaar-card-price,
.bazaar-card-qty {
font-weight: 500;
font-size: var(--large-font-size);
line-height: 1.1;
font-family: sans-serif;
color: var(--bazaar-text-primary);
}
.bazaar-card-bottom {
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid var(--bazaar-border-light);
padding-top: var(--spacing-xs);
margin-top: auto;
font-size: var(--small-font-size);
color: var(--bazaar-text-secondary);
}
.bazaar-card-time {
color: var(--bazaar-text-tertiary);
font-size: var(--tiny-font-size);
}
.bazaar-price-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
min-width: 0;
word-break: break-word;
overflow-wrap: anywhere;
width: 100%;
text-align: center;
}
.bazaar-market-diff {
display: block;
text-align: center;
margin: 0 auto;
word-break: break-word;
overflow-wrap: anywhere;
min-width: 0;
}
.bazaar-market-diff.positive {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.bazaar-market-diff.negative {
color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.bazaar-market-diff:hover {
transform: scale(1.05);
}
body.dark-mode .bazaar-market-diff.positive {
color: #f87171;
background: rgba(248, 113, 113, 0.15);
}
body.dark-mode .bazaar-market-diff.negative {
color: #4ade80;
background: rgba(74, 222, 128, 0.15);
}
.bazaar-footer {
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--bazaar-border-light);
font-size: var(--base-font-size);
color: var(--bazaar-text-secondary);
line-height: 1.4;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: var(--spacing-xs);
}
.bazaar-footer-stats,
.bazaar-footer-stat,
.bazaar-footer-link,
.bazaar-footer-separator {
font-size: var(--base-font-size);
}
.bazaar-footer-link-text-full {
display: inline;
}
.bazaar-footer-link-text-short {
display: none;
}
@media (max-width: 600px) {
.bazaar-footer-link-text-full {
display: none;
}
.bazaar-footer-link-text-short {
display: inline;
}
.bazaar-footer-stat-label {
display: none;
}
}
.bazaar-footer-link {
color: #0066cc !important;
text-decoration: none;
transition: opacity 0.2s;
}
.bazaar-footer-link:hover {
opacity: 0.8;
text-decoration: underline;
}
.bazaar-footer-link:visited {
color: #0066cc !important;
}
body.dark-mode .bazaar-footer-link {
color: #4da6ff !important;
}
body.dark-mode .bazaar-footer-link:visited {
color: #4da6ff !important;
}
`;
GM_addStyle(styles);
};
const utils = {
getRelativeTime(timestamp) {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
const intervals = [
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
{ unit: 'second', seconds: 1 }
];
for (const { unit, seconds } of intervals) {
const value = Math.floor(diff / seconds);
if (value >= 1) {
return `Checked ${value} ${unit}${value !== 1 ? 's' : ''} ago`;
}
}
return 'just now';
},
parseItemID(body) {
if (!body) return null;
const parseValue = (data) => {
if (data instanceof FormData) return data.get('itemID');
if (data instanceof URLSearchParams) return data.get('itemID');
if (typeof data === 'string') return new URLSearchParams(data).get('itemID');
return null;
};
return parseValue(body);
},
isMobile() {
return window.innerWidth <= CONFIG.MOBILE_BREAKPOINT;
},
createBazaarUrl(listing, itemID) {
return `https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemID=${itemID}&price=${listing.price}&qty=${listing.quantity}#/`;
}
};
const dom = {
createContainer() {
const containerHtml = `
<div class="bazaar-controls">
<div class="bazaar-filter">
<label>Min:</label>
<input type="number" class="min-qty" min="0" placeholder="0">
</div>
<div class="bazaar-sort-buttons">
<span class="bazaar-sort-label">Sort:</span>
<button class="bazaar-sort-btn" data-category="price" data-state="asc">
<span class="sort-text">Price</span>
<span class="sort-arrow">↑</span>
</button>
<button class="bazaar-sort-btn" data-category="qty" data-state="none">
<span class="sort-text">Qty</span>
<span class="sort-arrow"></span>
</button>
<button class="bazaar-sort-btn" data-category="time" data-state="none">
<span class="sort-text">Time</span>
<span class="sort-arrow"></span>
</button>
</div>
</div>
<div class="bazaar-display">
<button class="bazaar-nav prev" disabled>‹</button>
<div class="bazaar-cards-wrapper"></div>
<button class="bazaar-nav next" disabled>›</button>
</div>
<div class="bazaar-pagination-info">
<button class="bazaar-nav-first" title="Go to first page">««</button>
<span class="bazaar-page-info">Page <span class="current-page">1</span> of <span class="total-pages">1</span></span>
<button class="bazaar-nav-last" title="Go to last page">»»</button>
</div>
<div class="bazaar-footer">
<div class="bazaar-footer-stats"></div>
<span class="bazaar-footer-separator">•</span>
<a href="https://weav3r.dev/" target="_blank" class="bazaar-footer-link">
<span class="bazaar-footer-link-text-full">Register your bazaar at weav3r.dev</span>
<span class="bazaar-footer-link-text-short">Register @ weav3r.dev</span>
</a>
</div>
`;
return $('<li>').addClass('bazaar-container').html(containerHtml);
},
createBazaarCard(listing, itemID, marketPrice) {
const url = utils.createBazaarUrl(listing, itemID);
let linkAttrs = '';
switch (CONFIG.BAZAAR_CLICK_BEHAVIOR) {
case 'same-tab':
linkAttrs = '';
break;
case 'new-window':
linkAttrs = 'target="_blank" onclick="window.open(this.href, \'_blank\', \'width=800,height=600\'); return false;"';
break;
case 'new-tab':
default:
linkAttrs = 'target="_blank"';
break;
}
let marketComparisonHtml = '';
if (CONFIG.SHOW_MARKET_COMPARISON && marketPrice) {
const diff = listing.price - marketPrice;
const percentDiff = ((diff / marketPrice) * 100).toFixed(1);
const isAboveMarket = diff > 0;
const sign = isAboveMarket ? '+' : '';
const className = isAboveMarket ? 'positive' : 'negative';
marketComparisonHtml = `
<span class="bazaar-market-diff ${className}">
${sign}${percentDiff}%
</span>
`;
}
return `
<div class="bazaar-card">
<div class="bazaar-card-name">
<a href="${url}" ${linkAttrs}>${listing.player_name}</a>
</div>
<div class="bazaar-card-info">
<div class="bazaar-price-wrapper">
<div class="bazaar-card-price">$${listing.price.toLocaleString()}</div>
${marketComparisonHtml}
</div>
<div class="bazaar-card-qty">Qty: ${listing.quantity}</div>
</div>
<div class="bazaar-card-bottom">
<span class="bazaar-card-time">${utils.getRelativeTime(listing.last_checked)}</span>
</div>
</div>
`;
},
findSellerList() {
return $(CONFIG.SELECTORS.DESKTOP_LIST).length ?
$(CONFIG.SELECTORS.DESKTOP_LIST) :
$(CONFIG.SELECTORS.MOBILE_LIST);
},
insertContainer($sellerList, $container) {
try {
if (utils.isMobile()) {
const $itemsHeader = $(CONFIG.SELECTORS.MOBILE_HEADER);
const sellerListEl = document.querySelector(CONFIG.SELECTORS.MOBILE_LIST);
if ($itemsHeader.length) {
const existing = $itemsHeader.next('.bazaar-container');
if (existing.length) {
existing.remove();
}
const $mobileContainer = $('<div>')
.addClass('bazaar-container')
.html($container.html());
requestAnimationFrame(() => {
$itemsHeader.after($mobileContainer);
});
setTimeout(() => {
if (sellerListEl && $mobileContainer[0]) {
const observer = new MutationObserver(() => {
if (!document.body.contains(sellerListEl)) {
$mobileContainer.remove();
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 0);
return $mobileContainer;
} else {
const $firstRow = $sellerList.find(CONFIG.SELECTORS.MOBILE_ROW).first();
if ($firstRow.length) {
$firstRow.after($container);
} else {
$sellerList.prepend($container);
}
setTimeout(() => {
if (sellerListEl && $container[0]) {
const observer = new MutationObserver(() => {
if (!document.body.contains(sellerListEl)) {
$container.remove();
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 0);
}
} else {
$sellerList.prepend($container);
}
return $container;
} catch (error) {
console.error('Error inserting bazaar container:', error);
return $container;
}
}
};
class ListingController {
constructor($container, itemID) {
this.$container = $container;
this.itemID = itemID;
this.$cardsWrapper = $container.find('.bazaar-cards-wrapper');
this.$prevBtn = $container.find('.bazaar-nav.prev');
this.$nextBtn = $container.find('.bazaar-nav.next');
this.$firstBtn = $container.find('.bazaar-nav-first');
this.$lastBtn = $container.find('.bazaar-nav-last');
this.$currentPage = $container.find('.current-page');
this.$totalPages = $container.find('.total-pages');
this.$minQty = $container.find('.min-qty');
this.$sortButtons = $container.find('.bazaar-sort-btn');
this.$footerStats = $container.find('.bazaar-footer-stats');
this.allListings = [];
this.itemData = null;
this.currentPage = 0;
this.currentSort = CONFIG.DEFAULT_SORT;
$container.data('controller', this);
if (CONFIG.DEFAULT_MIN_QTY && CONFIG.DEFAULT_MIN_QTY > 0) {
this.$minQty.val(CONFIG.DEFAULT_MIN_QTY);
}
this.setInitialSortState();
this.setupEventHandlers();
this.loadData();
}
setInitialSortState() {
const [category, direction] = CONFIG.DEFAULT_SORT.split('-');
this.$sortButtons.each(function() {
$(this).data('state', 'none').attr('data-state', 'none')
.find('.sort-arrow').text('');
});
const $defaultBtn = this.$sortButtons.filter(`[data-category="${category}"]`);
if ($defaultBtn.length) {
const arrow = direction === 'asc' ? '↑' : '↓';
$defaultBtn.data('state', direction).attr('data-state', direction)
.find('.sort-arrow').text(arrow);
}
}
setupEventHandlers() {
this.$minQty.on('input', () => {
this.currentPage = 0;
this.displayListings();
});
this.$sortButtons.on('click', (e) => {
const $btn = $(e.currentTarget);
this.handleSort($btn);
});
this.$prevBtn.on('click', () => this.navigate(-1));
this.$nextBtn.on('click', () => this.navigate(1));
this.$firstBtn.on('click', () => this.goToFirstPage());
this.$lastBtn.on('click', () => this.goToLastPage());
this.setupSwipeGestures();
}
handleSort($btn) {
const category = $btn.data('category');
const currentState = $btn.data('state');
this.$sortButtons.not($btn).each(function() {
$(this).data('state', 'none').attr('data-state', 'none')
.find('.sort-arrow').text('');
});
const states = ['none', 'asc', 'desc'];
const arrows = ['', '↑', '↓'];
const currentIndex = states.indexOf(currentState);
const newIndex = (currentIndex + 1) % states.length;
const newState = states[newIndex];
$btn.data('state', newState).attr('data-state', newState);
$btn.find('.sort-arrow').text(arrows[newIndex]);
this.currentSort = newState !== 'none' ?
`${category}-${newState}` : CONFIG.DEFAULT_SORT;
this.currentPage = 0;
this.displayListings();
}
navigate(direction) {
const filtered = this.getFilteredListings();
const maxPage = Math.ceil(filtered.length / CONFIG.ITEMS_PER_PAGE) - 1;
this.currentPage = Math.max(0, Math.min(maxPage, this.currentPage + direction));
this.displayListings();
}
goToFirstPage() {
this.currentPage = 0;
this.displayListings();
}
goToLastPage() {
const filtered = this.getFilteredListings();
const maxPage = Math.max(0, Math.ceil(filtered.length / CONFIG.ITEMS_PER_PAGE) - 1);
this.currentPage = maxPage;
this.displayListings();
}
setupSwipeGestures() {
let touchStartX = 0;
let touchStartY = 0;
let touchEndX = 0;
let touchEndY = 0;
const minSwipeDistance = 50;
const maxVerticalDistance = 100;
const swipeArea = this.$container.find('.bazaar-display')[0];
if (!swipeArea) return;
swipeArea.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
}, { passive: true });
swipeArea.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY;
this.handleSwipe();
}, { passive: true });
const handleSwipe = () => {
const horizontalDistance = touchEndX - touchStartX;
const verticalDistance = Math.abs(touchEndY - touchStartY);
if (Math.abs(horizontalDistance) > minSwipeDistance && verticalDistance < maxVerticalDistance) {
if (horizontalDistance > 0) {
this.navigate(-1);
} else {
this.navigate(1);
}
}
};
this.handleSwipe = handleSwipe;
}
async loadData() {
const cacheKey = `item_${this.itemID}`;
const cached = state.apiCache.get(cacheKey);
const now = Date.now();
if (cached && (now - cached.timestamp) < CONFIG.CACHE_DURATION) {
this.allListings = cached.listings || [];
this.itemData = cached.itemData || null;
this.updateFooter();
this.displayListings();
return;
}
try {
const data = await this.fetchData();
this.allListings = data.listings || [];
this.itemData = data;
state.apiCache.set(cacheKey, {
listings: this.allListings,
itemData: data,
timestamp: now
});
this.updateFooter();
this.displayListings();
} catch (error) {
this.showError('Error loading data');
}
}
async fetchData() {
try {
const response = await PDA.httpRequest({
method: 'GET',
url: `https://weav3r.dev/api/marketplace/${this.itemID}`,
headers: { 'Accept': 'application/json' }
});
if (response.status === 200) {
return JSON.parse(response.responseText);
} else {
throw new Error(`API returned status ${response.status}`);
}
} catch (error) {
console.error('Failed to fetch bazaar data:', error);
throw error;
}
}
getFilteredListings() {
const minQty = parseInt(this.$minQty.val()) || 0;
let filtered = this.allListings.filter(listing => listing.quantity >= minQty);
const [category, direction] = this.currentSort.split('-');
const multiplier = direction === 'desc' ? -1 : 1;
const sortFunctions = {
price: (a, b) => (a.price - b.price) * multiplier,
qty: (a, b) => (a.quantity - b.quantity) * multiplier,
time: (a, b) => (a.last_checked - b.last_checked) * multiplier
};
if (sortFunctions[category]) {
filtered.sort(sortFunctions[category]);
}
return filtered;
}
displayListings() {
const filtered = this.getFilteredListings();
const start = this.currentPage * CONFIG.ITEMS_PER_PAGE;
const pageItems = filtered.slice(start, start + CONFIG.ITEMS_PER_PAGE);
const totalPages = Math.max(1, Math.ceil(filtered.length / CONFIG.ITEMS_PER_PAGE));
this.$cardsWrapper.empty();
if (CONFIG.ITEMS_PER_PAGE > 4) {
this.$cardsWrapper.addClass('multi-row');
} else {
this.$cardsWrapper.removeClass('multi-row');
}
if (pageItems.length > 0) {
const marketPrice = this.itemData?.market_price || null;
const cards = pageItems.map(listing =>
dom.createBazaarCard(listing, this.itemID, marketPrice)
).join('');
this.$cardsWrapper.html(cards);
} else if (filtered.length === 0) {
this.showEmpty();
}
const hasNext = start + CONFIG.ITEMS_PER_PAGE < filtered.length;
const maxPage = Math.max(0, totalPages - 1);
this.$prevBtn.prop('disabled', this.currentPage === 0);
this.$nextBtn.prop('disabled', !hasNext);
this.$firstBtn.prop('disabled', this.currentPage === 0);
this.$lastBtn.prop('disabled', this.currentPage === maxPage);
this.$currentPage.text(this.currentPage + 1);
this.$totalPages.text(totalPages);
}
showError(message) {
this.$cardsWrapper.html(`<div class="bazaar-error">${message}</div>`);
}
showEmpty() {
this.$cardsWrapper.html('<div class="bazaar-empty">No bazaar listings found</div>');
}
updateFooter() {
if (!this.itemData) return;
const itemName = this.itemData.item_name || 'Unknown Item';
const marketPrice = this.itemData.market_price;
let statsHtml = `
<span class="bazaar-footer-stat"><strong>${itemName}</strong> [${this.itemID}]</span>
`;
if (marketPrice) {
statsHtml += `<span class="bazaar-footer-stat"><span class="bazaar-footer-stat-label">Market: </span><strong>$${marketPrice.toLocaleString()}</strong></span>`;
}
this.$footerStats.html(statsHtml);
}
}
let displayDebounceTimer = null;
function interceptFetch() {
try {
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
state.originalFetch = targetWindow.fetch;
if (!state.originalFetch) {
console.error('[Bazaar Listings] fetch function not found');
return;
}
targetWindow.fetch = async function(...args) {
const [resource, config] = args;
if (resource?.includes('page.php?sid=iMarket&step=getListing')) {
const itemID = utils.parseItemID(config?.body);
if (itemID) {
const response = await state.originalFetch.apply(this, args);
if (displayDebounceTimer) {
clearTimeout(displayDebounceTimer);
}
displayDebounceTimer = setTimeout(() => {
displayItemID(itemID);
displayDebounceTimer = null;
}, 150);
return response;
}
}
return state.originalFetch.apply(this, args);
};
} catch (error) {
console.error('[Bazaar Listings] Error intercepting fetch:', error);
}
}
let containerCreationInProgress = false;
function displayItemID(itemID) {
if (containerCreationInProgress) {
return;
}
if (state.currentItemID === itemID && state.currentDisplay && state.currentDisplay.parent().length) {
return;
}
containerCreationInProgress = true;
try {
state.currentItemID = itemID;
$('.bazaar-container').each(function() {
const controller = $(this).data('controller');
if (controller) {
$(this).removeData('controller');
}
$(this).remove();
});
$('.rowWrapper___OrFGK .bazaar-container, .itemsHeader___ZTO9r + .bazaar-container, .itemsHeader___ZTO9r + div.bazaar-container').remove();
state.currentDisplay = null;
const $sellerList = dom.findSellerList();
if (!$sellerList.length) {
if (!state.retryCount) state.retryCount = 0;
state.retryCount++;
if (state.retryCount < 5) {
setTimeout(() => {
containerCreationInProgress = false;
if (!state.currentDisplay && state.currentItemID === itemID) {
displayItemID(itemID);
}
}, 500);
} else {
console.error('Could not find seller list after 5 attempts');
state.retryCount = 0;
containerCreationInProgress = false;
}
return;
}
state.retryCount = 0;
if ($('.bazaar-container').length > 0) {
containerCreationInProgress = false;
return;
}
const $container = dom.createContainer();
state.currentDisplay = dom.insertContainer($sellerList, $container);
const containerCount = $('.bazaar-container').length;
if (containerCount > 1) {
$('.bazaar-container').not(state.currentDisplay).remove();
}
new ListingController(state.currentDisplay, itemID);
} finally {
setTimeout(() => {
containerCreationInProgress = false;
}, 100);
}
}
const bazaar = {
parseUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
return {
itemID: urlParams.get('itemID'),
expectedPrice: parseInt(urlParams.get('price')),
expectedQty: parseInt(urlParams.get('qty'))
};
},
findItemInBazaar(itemID) {
const selectors = [
`[data-item="${itemID}"]`,
`[data-itemid="${itemID}"]`,
`.item___GYCYJ:has(img[src*="/items/${itemID}/"])`,
`.item___khvF6:has(img[src*="/items/${itemID}/"])`
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) return element;
}
return null;
},
scrollToItem(element) {
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const elementCenter = rect.top + rect.height / 2;
const targetScroll = window.pageYOffset + elementCenter - viewportHeight / 2;
window.scrollTo({
top: Math.max(0, targetScroll),
behavior: 'smooth'
});
},
highlightItem(element) {
element.style.cssText += `
outline: 3px solid #FFD700 !important;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.6) !important;
background-color: rgba(255, 215, 0, 0.1) !important;
position: relative !important;
z-index: 100 !important;
animation: bazaar-pulse 2s ease-in-out infinite !important;
`;
},
extractItemPrice(element) {
const priceSelectors = [
'.price___dJqda',
'.price',
'[class*="price"]',
'p:nth-child(2)'
];
for (const selector of priceSelectors) {
const priceElement = element.querySelector(selector);
if (priceElement) {
const priceText = priceElement.textContent;
const price = parseInt(priceText.replace(/[$,]/g, ''));
if (!isNaN(price)) return price;
}
}
return null;
},
extractItemQty(element) {
const qtySelectors = [
'.amountValue___cSVqO',
'.amount___K8sOQ',
'[class*="amount"]'
];
for (const selector of qtySelectors) {
const qtyElement = element.querySelector(selector);
if (qtyElement) {
const qtyText = qtyElement.textContent;
const qty = parseInt(qtyText.replace(/[^\d]/g, ''));
if (!isNaN(qty)) return qty;
}
}
return null;
},
showToast(message, type = 'warning') {
const toast = document.createElement('div');
toast.className = 'bazaar-toast';
const bgColor = type === 'error' ? '#ff4444' : type === 'warning' ? '#ff9944' : '#44bb44';
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${bgColor};
color: white;
padding: 16px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
z-index: 2147483647;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: bazaar-toast-slide 0.3s ease-out;
max-width: 400px;
line-height: 1.4;
`;
toast.innerHTML = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'bazaar-toast-fade 0.3s ease-out forwards';
setTimeout(() => toast.remove(), 300);
}, 5000);
},
showPriceWarning(actualPrice, expectedPrice, element) {
const difference = actualPrice - expectedPrice;
const percentDiff = ((difference / expectedPrice) * 100).toFixed(1);
const isHigher = difference > 0;
const message = `⚠️ Price ${isHigher ? 'increased' : 'decreased'} by $${Math.abs(difference).toLocaleString()} (${isHigher ? '+' : ''}${percentDiff}%)`;
const warningDiv = document.createElement('div');
warningDiv.className = 'bazaar-price-warning';
warningDiv.style.cssText = `
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background: ${isHigher ? '#ff4444' : '#ff9944'};
color: white;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
z-index: 1000;
white-space: nowrap;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
animation: bazaar-warning-bounce 0.5s ease-out;
`;
warningDiv.innerHTML = message;
element.style.position = 'relative';
element.appendChild(warningDiv);
},
addAnimationStyles() {
if (!document.querySelector('#bazaar-highlight-styles')) {
const style = document.createElement('style');
style.id = 'bazaar-highlight-styles';
style.textContent = `
@keyframes bazaar-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
@keyframes bazaar-warning-bounce {
0% { transform: translateX(-50%) translateY(-10px); opacity: 0; }
100% { transform: translateX(-50%) translateY(0); opacity: 1; }
}
@keyframes bazaar-toast-slide {
0% { transform: translateX(400px); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
@keyframes bazaar-toast-fade {
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(100px); }
}
.bazaar-price-warning::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid inherit;
}
`;
document.head.appendChild(style);
}
},
init() {
if (!window.location.pathname.includes('bazaar.php')) return;
const params = this.parseUrlParams();
if (!params.itemID || !params.expectedPrice) return;
this.addAnimationStyles();
let attempts = 0;
const maxAttempts = 10;
const attemptHighlight = () => {
if (attempts >= maxAttempts) {
this.showToast('❌ Item not found in this bazaar! It may have been sold or removed.', 'error');
return;
}
attempts++;
try {
const itemElement = this.findItemInBazaar(params.itemID);
if (itemElement) {
requestAnimationFrame(() => {
this.scrollToItem(itemElement);
this.highlightItem(itemElement);
const actualPrice = this.extractItemPrice(itemElement);
if (actualPrice && actualPrice !== params.expectedPrice) {
this.showPriceWarning(actualPrice, params.expectedPrice, itemElement);
}
});
} else {
const delay = utils.isMobile() ? 1000 : 500;
setTimeout(attemptHighlight, delay);
}
} catch (error) {
console.error('Error in bazaar highlight:', error);
this.showToast('❌ An error occurred while highlighting the item.', 'error');
}
};
setTimeout(attemptHighlight, utils.isMobile() ? 500 : 100);
}
};
const settings = {
createModal() {
const modalHtml = `
<div class="bazaar-settings-modal" id="bazaarSettingsModal">
<div class="bazaar-settings-content">
<div class="bazaar-settings-header">
<h1 class="bazaar-settings-title">Bazaar Listings Settings</h1>
<button class="bazaar-settings-close">×</button>
</div>
<div class="bazaar-settings-body">
<div class="bazaar-settings-section">
<h2 class="bazaar-settings-section-title">Display Settings</h2>
<div class="bazaar-settings-field">
<label class="bazaar-settings-field-label">Items Per Page</label>
<p class="bazaar-settings-field-description">Number of bazaar listings to show per page</p>
<input type="number" class="bazaar-settings-input" id="itemsPerPage" min="1" max="10" value="${CONFIG.ITEMS_PER_PAGE}">
</div>
<div class="bazaar-settings-field">
<label class="bazaar-settings-field-label">Default Sort</label>
<p class="bazaar-settings-field-description">Choose how listings are sorted: Price, Quantity, Profit, or Last Updated</p>
<select class="bazaar-settings-select" id="defaultSort">
<option value="price-asc" ${CONFIG.DEFAULT_SORT === 'price-asc' ? 'selected' : ''}>Price (Low to High)</option>
<option value="price-desc" ${CONFIG.DEFAULT_SORT === 'price-desc' ? 'selected' : ''}>Price (High to Low)</option>
<option value="qty-asc" ${CONFIG.DEFAULT_SORT === 'qty-asc' ? 'selected' : ''}>Quantity (Low to High)</option>
<option value="qty-desc" ${CONFIG.DEFAULT_SORT === 'qty-desc' ? 'selected' : ''}>Quantity (High to Low)</option>
<option value="time-asc" ${CONFIG.DEFAULT_SORT === 'time-asc' ? 'selected' : ''}>Time (Oldest First)</option>
<option value="time-desc" ${CONFIG.DEFAULT_SORT === 'time-desc' ? 'selected' : ''}>Time (Newest First)</option>
</select>
</div>
<div class="bazaar-settings-field">
<label class="bazaar-settings-field-label">Default Minimum Quantity</label>
<p class="bazaar-settings-field-description">Default minimum quantity filter value</p>
<input type="number" class="bazaar-settings-input" id="defaultMinQty" min="0" value="${CONFIG.DEFAULT_MIN_QTY || 0}">
</div>
</div>
<div class="bazaar-settings-section">
<h2 class="bazaar-settings-section-title">Features</h2>
<div class="bazaar-settings-toggle-field">
<div class="bazaar-settings-toggle-info">
<div class="bazaar-settings-field-label">Show Market Price Comparison</div>
<div class="bazaar-settings-field-description">Display price difference from market average</div>
</div>
<div class="bazaar-toggle-switch ${CONFIG.SHOW_MARKET_COMPARISON ? 'active' : ''}" id="marketComparisonToggle">
<div class="bazaar-toggle-slider"></div>
</div>
</div>
</div>
<div class="bazaar-settings-section">
<h2 class="bazaar-settings-section-title">Behavior</h2>
<div class="bazaar-settings-field">
<label class="bazaar-settings-field-label">Bazaar Link Behavior</label>
<p class="bazaar-settings-field-description">How bazaar links should open when clicked</p>
<select class="bazaar-settings-select" id="clickBehavior">
<option value="same-tab" ${CONFIG.BAZAAR_CLICK_BEHAVIOR === 'same-tab' ? 'selected' : ''}>Same Tab</option>
<option value="new-tab" ${CONFIG.BAZAAR_CLICK_BEHAVIOR === 'new-tab' ? 'selected' : ''}>New Tab</option>
<option value="new-window" ${CONFIG.BAZAAR_CLICK_BEHAVIOR === 'new-window' ? 'selected' : ''}>New Window</option>
</select>
</div>
</div>
</div>
<div class="bazaar-settings-actions">
<button class="bazaar-settings-btn bazaar-settings-btn-secondary" id="cancelSettingsBtn">Cancel</button>
<button class="bazaar-settings-btn bazaar-settings-btn-primary" id="saveSettingsBtn">Save</button>
</div>
<div class="bazaar-settings-credits">
Created by <a href="https://www.torn.com/profiles.php?XID=1185324" target="_blank">Weav3r [1185324]</a>
<span class="bazaar-settings-credits-divider">•</span>
Data sourced from <a href="https://weav3r.dev" target="_blank">weav3r.dev</a>
</div>
</div>
</div>
`;
$('body').append(modalHtml);
$('.bazaar-settings-close, #cancelSettingsBtn').on('click', () => {
$('#bazaarSettingsModal').removeClass('show');
});
$('#saveSettingsBtn').on('click', () => {
this.save();
});
$('#marketComparisonToggle').on('click', function() {
$(this).toggleClass('active');
});
$('#bazaarSettingsModal').on('click', function(e) {
if (e.target === this) {
$(this).removeClass('show');
}
});
},
injectSettingsButton() {
if (document.querySelector('#bazaarSettingsBtn')) {
return;
}
const settingsMenu = document.querySelector('.settings-menu');
if (!settingsMenu) {
const altMenu = document.querySelector('ul[class*="settings-menu"]') ||
document.querySelector('[class*="settingsMenu"]') ||
document.querySelector('ul.settings-menu');
if (altMenu) {
settings.injectButtonIntoMenu(altMenu);
}
return;
}
this.injectButtonIntoMenu(settingsMenu);
},
injectButtonIntoMenu(settingsMenu) {
const settingsButton = document.createElement('li');
settingsButton.className = 'link';
settingsButton.id = 'bazaarSettingsBtn';
settingsButton.innerHTML = `
<a href="#" class="bazaar-settings-link">
<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" class="default___XXAGt" filter="" fill="currentColor" stroke="transparent" stroke-width="0" width="24" height="24" viewBox="0 0 24 24">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
</div>
<span class="link-text">Bazaar Settings</span>
</a>
`;
settingsButton.querySelector('.bazaar-settings-link').addEventListener('click', (e) => {
e.preventDefault();
const modal = document.getElementById('bazaarSettingsModal');
if (modal) {
modal.classList.add('show');
} else {
console.error('Bazaar settings modal not found');
this.createModal();
setTimeout(() => {
document.getElementById('bazaarSettingsModal').classList.add('show');
}, 100);
}
});
const logoutItem = Array.from(settingsMenu.querySelectorAll('a')).find(a =>
a.href && a.href.includes('logout.php'))?.parentElement;
if (logoutItem) {
settingsMenu.insertBefore(settingsButton, logoutItem);
} else {
settingsMenu.appendChild(settingsButton);
}
},
save() {
const newSettings = {
ITEMS_PER_PAGE: parseInt($('#itemsPerPage').val()) || 3,
DEFAULT_SORT: $('#defaultSort').val() || 'price-asc',
DEFAULT_MIN_QTY: parseInt($('#defaultMinQty').val()) || 0,
SHOW_MARKET_COMPARISON: $('#marketComparisonToggle').hasClass('active'),
BAZAAR_CLICK_BEHAVIOR: $('#clickBehavior').val() || 'new-tab'
};
const mergedSettings = { ...CONFIG, ...newSettings };
saveSettings(mergedSettings);
$('#bazaarSettingsModal').removeClass('show');
this.showToast('Settings saved successfully!', 'success');
if (state.currentDisplay) {
const controller = state.currentDisplay.data('controller');
if (controller) {
controller.currentSort = CONFIG.DEFAULT_SORT;
controller.displayListings();
}
}
},
showToast(message, type = 'success') {
const toast = $('<div>').addClass('bazaar-settings-toast').css({
position: 'fixed',
bottom: '20px',
right: '20px',
background: type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3',
color: 'white',
padding: '16px 24px',
borderRadius: '4px',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
zIndex: 10001,
fontSize: '14px',
fontWeight: '500',
opacity: 0,
transform: 'translateY(20px)',
transition: 'all 0.3s'
}).text(message);
$('body').append(toast);
setTimeout(() => {
toast.css({
opacity: 1,
transform: 'translateY(0)'
});
}, 10);
setTimeout(() => {
toast.css({
opacity: 0,
transform: 'translateY(20px)'
});
setTimeout(() => toast.remove(), 300);
}, 3000);
}
};
async function init() {
try {
injectStyles();
await PDA.detect();
if (PDA.isDetected) {
$('body').attr('data-torn-pda', 'true');
}
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (typeof targetWindow.fetch !== 'undefined') {
interceptFetch();
} else {
}
settings.createModal();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => settings.injectSettingsButton(), 1000);
});
} else {
setTimeout(() => settings.injectSettingsButton(), 1000);
}
$(document).ready(() => {
setTimeout(() => {
if (!document.querySelector('#bazaarSettingsBtn')) {
settings.injectSettingsButton();
}
}, 2000);
});
if (window.location.pathname.includes('bazaar.php')) {
bazaar.init();
}
} catch (error) {
console.error('Error initializing:', error);
}
}
if (navigator.userAgent && navigator.userAgent.includes('TornPDA')) {
setTimeout(init, 500);
} else {
init();
}
})(jQuery);