BazNav

An enhanced Torn City bazaar navigator with customization options to make this tool YOURS

// ==UserScript==
// @name         BazNav
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  An enhanced Torn City bazaar navigator with customization options to make this tool YOURS
// @author       J4C
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// ==/UserScript==

(() => {
    'use strict';

    const defaultSettings = {
        // UI Colors
        gradientStart: '#000000', gradientEnd: '#ffffff',
        textColor: '#ffffff', fontFamily: 'Arial, sans-serif',
        fontSize: 14, opacity: 1, width: 200,
        borderRadius: 8, borderWidth: 1, borderColor: '#000000',
        shadowBlur: 5, shadowColor: 'rgba(0,0,0,0.2)',

        // Buttons
        buttonBgColor: '#ffffff', buttonTextColor: '#000000',
        buttonBorderColor: '#000000', buttonBorderWidth: 1,
        buttonHoverColor: '#8F4729',

        // Counter
        counterBgColor: '#252525', counterTextColor: '#fff',
        counterBorderColor: '#fff',

        // Settings Panel
        panelBgColor: '#ffffff', panelTextColor: '#000',
        panelBorderColor: '#cccccc',

        // Loading Bar
        loadingBarColor: '#8F4729'
    };

    if (window.bazNavInitialized) {
        console.warn('BazNav is already initialized!');
        return;
    }
    window.bazNavInitialized = true;

    // User IDs
    const userIds = [
        1010587, 3199521, 1281694, 1286142, 1601153, 1821105, 1826175, 1853324, 1927218, 1962806, 2018311,
        2018693, 2029519, 2093595, 2144418, 2145030, 2150517, 2157199, 2163550, 2176411, 2202548,
        2203576, 2214797, 2215721, 2256247, 2263400, 2271357, 2321305, 2327316, 2329817, 2332873,
        2334174, 2349680, 2352900, 2373781, 2418443, 2459465, 2462160, 2466069, 2470308, 2515770,
        2531848, 2541678, 2557282, 2561006, 2570451, 2587064, 2596546, 2599031, 2601828, 2631792,
        2637146, 2649236, 2656557, 2658357, 2659552, 2664822, 2668560, 2675624, 2676295, 2693254,
        2693850, 2700933, 2706250, 2718606, 2733754, 2746056, 2749015, 2759415, 2768219, 2769269,
        2810688, 2812113, 2821007, 2855343, 2858099, 286232, 2865837, 2871891, 2905897, 2930086,
        2954100, 2954103, 2962007, 3025664, 3050251, 3060802, 3108759, 3118416, 3145984, 3146495,
        3152626, 3166857, 3169682, 3170980, 3181495, 3182441, 3185895, 3186599, 3186796, 3187167,
        3187441, 3192720, 3198458, 3200247, 3218273, 3220837, 3237207, 3244939, 3248078,
        3249592, 3253085, 3253836, 3256697, 3259246, 3272175, 3276048, 3284969, 3303003, 3304611,
        3304959, 3306975, 3325064, 3325774, 3327900, 333493, 3338586, 3340959, 3347008, 3348231,
        3351015, 3355475, 3357431, 3369647, 3372209, 3384244, 3385583, 3388276, 3390097, 3392180,
        3394866, 3399085, 3400186, 3405216, 3407522, 3409934, 3424078, 3443006, 3444185, 3445243,
        3447101, 3452004, 3455398, 3455607, 3455822, 3462112, 3465149, 3466754, 3468210, 3469314,
        3474044, 3476656, 3480404, 3484482, 3484674, 3485561, 3488406, 3492821, 3493798, 3499388,
        3504237, 3506751, 3528214, 3532145, 3532830, 3535815, 3546645, 3552837, 3554441, 3555668,
        3561791, 3562619, 3570456, 3571449, 3582325, 3584826, 3588663, 3595133, 3602729, 360330,
        3603393, 3615849, 3617287, 3621114, 3625719, 3626448, 3626655, 3653078, 3657829, 3661330,
        3665796, 3676143, 3738196, 830027, 1145056, 1403609, 1441750, 1496324, 1636350
];

const uniqueUserIds = [...new Set(userIds)];
const originalLinks = uniqueUserIds.map(id => `https://www.torn.com/bazaar.php?userId=${id}`);

const links = new Proxy(originalLinks, {
    get(target, prop) {
        if (prop === 'length') {
            console.log('Links array length accessed:', target.length);
        } else if (prop === 'map') {
            return function(...args) {
                console.log('Links array map called, length:', target.length);
                return Array.prototype.map.apply(target, args);
            };
        }
        return target[prop];
    },
    set() {
        console.error('Attempted to modify frozen links array! Stack:', new Error().stack);
        return false; // Prevent modifications
    }
});

console.log(`Total User IDs before deduplication: ${userIds.length}`);
console.log(`Total Unique User IDs: ${uniqueUserIds.length}`);
console.log(`Total Links: ${links.length}`);
console.log('Links array created at:', new Error().stack);

let index = Math.min(+localStorage.getItem('bazaarLinkIndex') || 0, links.length - 1);

const settings = JSON.parse(localStorage.getItem('bazaarNavSettings')) || defaultSettings;

    // Initialize GM.addStyle if not available
    if (typeof GM === 'undefined') window.GM = {};
    GM.addStyle ||= css => document.head.appendChild(Object.assign(document.createElement('style'), {textContent: css}));

    // Create UI elements
    const container = document.createElement('div');
    container.id = 'bazaarNavFloat';

    const counter = document.createElement('div');
    counter.id = 'bazaarCounter';
    counter.textContent = `${index + 1} / ${links.length}`;
console.log('Initial links length when creating counter:', links.length);
    const cogIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    cogIcon.setAttribute("id", "cogIcon");
    cogIcon.setAttribute("viewBox", "0 0 24 24");
    cogIcon.setAttribute("aria-label", "Settings");
    cogIcon.setAttribute("role", "button");
    cogIcon.setAttribute("tabindex", "0");
    cogIcon.innerHTML = `<path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 00.12-.63l-1.92-3.32a.5.5 0 00-.6-.22l-2.39.96a7.027 7.027 0 00-1.63-.94l-.36-2.54a.5.5 0 00-.5-.42h-3.84a.5.5 0 00-.5.42l-.36 2.54a6.94 6.94 0 00-1.63.94l-2.39-.96a.5.5 0 00-.6.22l-1.92 3.32a.5.5 0 00.12.63l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.5.5 0 00-.12.63l1.92 3.32a.5.5 0 00.6.22l2.39-.96c.5.38 1.04.7 1.63.94l.36 2.54a.5.5 0 00.5.42h3.84a.5.5 0 00.5-.42l.36-2.54c.59-.24 1.13-.56 1.63-.94l2.39.96a.5.5 0 00.6-.22l1.92-3.32a.5.5 0 00-.12-.63l-2.03-1.58zM12 15.5a3.5 3.5 0 110-7 3.5 3.5 0 010 7z"/>`;

    const nextBtn = document.createElement('button');
    nextBtn.textContent = 'Next Bazaar';
    const backToFirstBtn = document.createElement('button');
    backToFirstBtn.textContent = 'Back to First';
    const settingsPanel = document.createElement('div');
    settingsPanel.id = 'settingsPanel';

    // Resize handle
    const resizeHandle = document.createElement('div');
    resizeHandle.id = 'resizeHandle';
    resizeHandle.innerHTML = '↔';
    resizeHandle.title = "Drag to resize";

    // Build UI
    counter.appendChild(cogIcon);
    [counter, nextBtn, backToFirstBtn, settingsPanel, resizeHandle].forEach(el => container.appendChild(el));
    document.body.appendChild(container);

    const savedPos = JSON.parse(localStorage.getItem('bazaarNavPosition'));
    if (savedPos?.x !== undefined && savedPos?.y !== undefined) {
        container.style.left = `${savedPos.x}px`;
        container.style.top = `${savedPos.y}px`;
    }

    const updateStyles = () => {
        GM.addStyle(`
            #bazaarNavFloat {
                z-index: 999999; position: fixed; top: 120px; left: 10px; display: flex;
                flex-direction: column; align-items: center; font-weight: bold;
                overflow: hidden; user-select: none; cursor: grab;
                background: linear-gradient(135deg, ${settings.gradientStart}, ${settings.gradientEnd});
                color: ${settings.textColor}; font-family: ${settings.fontFamily};
                font-size: ${settings.fontSize}px; border-radius: ${settings.borderRadius}px;
                border: ${settings.borderWidth}px solid ${settings.borderColor};
                box-shadow: 0 2px ${settings.shadowBlur}px ${settings.shadowColor};
                opacity: ${settings.opacity}; width: ${settings.width}px;
            }
            #bazaarNavFloat button {
                background: ${settings.buttonBgColor}; color: ${settings.buttonTextColor};
                border: ${settings.buttonBorderWidth}px solid ${settings.buttonBorderColor};
                padding: 8px 16px; cursor: pointer; font-weight: bold;
                font-size: ${settings.fontSize}px; width: calc(100% - 16px);
                text-shadow: 0 1px 1px #fff; border-radius: ${settings.borderRadius}px;
                transition: background 0.3s; margin: 4px 8px 0;
            }
            #bazaarNavFloat button:hover { background: ${settings.buttonHoverColor}; }
            #bazaarCounter {
                padding: 10px 14px; background: ${settings.counterBgColor};
                color: ${settings.counterTextColor}; width: calc(100% - 28px);
                text-align: center; border-bottom: 1px solid ${settings.counterBorderColor};
                position: relative; user-select: none; display: flex;
                align-items: center; justify-content: center; gap: 6px;
            }
            #bazaarCounter.loading::after {
                content: ''; position: absolute; bottom: 0; left: 0; height: 3px; width: 100%;
                background: ${settings.loadingBarColor}; animation: loadingBar 3s linear forwards;
                border-radius: 0 0 ${settings.borderRadius}px ${settings.borderRadius}px;
            }
            @keyframes loadingBar { from { width: 0; } to { width: 100%; } }
            #settingsPanel {
                display: none; background: ${settings.panelBgColor};
                color: ${settings.panelTextColor}; width: calc(100% - 16px);
                padding: 8px; box-sizing: border-box; border-top: 1px solid ${settings.panelBorderColor};
                font-size: 12px; max-height: 240px; overflow-y: auto; margin: 0 8px;
            }
            #settingsPanel label { display: block; margin: 8px 0 2px; font-weight: bold; }
            #settingsPanel input, #settingsPanel select {
                width: 100%; padding: 2px 4px; box-sizing: border-box; font-size: 13px;
                border-radius: 4px; border: 1px solid #ccc;
            }
            #settingsPanel .section {
                margin: 10px 0; padding-bottom: 10px;
                border-bottom: 1px dashed #ddd;
            }
            #settingsPanel .section-title {
                font-weight: bold; margin-bottom: 8px; color: #555;
            }
            #cogIcon {
                cursor: pointer; width: 18px; height: 18px; fill: ${settings.textColor};
                user-select: none; flex-shrink: 0;
            }
            #resizeHandle {
                position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
                background: rgba(0,0,0,0.1); cursor: nwse-resize; display: flex;
                align-items: center; justify-content: center; font-size: 12px;
                border-radius: 4px 0 0 0;
            }
            #resizeHandle:hover { background: rgba(0,0,0,0.2); }
            @media screen and (max-width: 1000px) {
                #bazaarNavFloat { top: 140px; }
            }
        `);
    };

    updateStyles();
    container.style.width = `${settings.width}px`;
container.style.opacity = settings.opacity;

    // Dragging function
    let isDragging = false, dragStartX, dragStartY, elemStartX, elemStartY;
    container.addEventListener('mousedown', e => {
        if (e.target.closest('#settingsPanel, #cogIcon, #resizeHandle')) return;
        isDragging = true;
        [dragStartX, dragStartY] = [e.clientX, e.clientY];
        const rect = container.getBoundingClientRect();
        [elemStartX, elemStartY] = [rect.left, rect.top];
        container.style.cursor = 'grabbing';
        e.preventDefault();
    });

    // Resize function
    let isResizing = false, startWidth, startX;
    resizeHandle.addEventListener('mousedown', e => {
        isResizing = true;
        startWidth = container.offsetWidth;
        startX = e.clientX;
        e.preventDefault();
        e.stopPropagation();
    });

    const handleMove = e => {
        if (isDragging) {
            let newX = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, elemStartX + e.clientX - dragStartX));
            let newY = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, elemStartY + e.clientY - dragStartY));
            container.style.left = `${newX}px`;
            container.style.top = `${newY}px`;
        } else if (isResizing) {
            const newWidth = Math.max(120, Math.min(400, startWidth + (e.clientX - startX)));
            settings.width = newWidth;
            container.style.width = `${newWidth}px`;
            saveSettings();
        }
    };

    const handleUp = () => {
        if (isDragging) {
            isDragging = false;
            container.style.cursor = 'grab';
            const rect = container.getBoundingClientRect();
            localStorage.setItem('bazaarNavPosition', JSON.stringify({x: rect.left, y: rect.top}));
        } else if (isResizing) {
            isResizing = false;
        }
    };

    window.addEventListener('mousemove', handleMove);
    window.addEventListener('mouseup', handleUp);

    // Navigation function
   const openLinkAtIndex = i => {
    console.log('openLinkAtIndex - links length:', links.length, 'index:', i);
    if (i < 0 || i >= links.length) {
        console.error('Invalid index or links array is empty');
        return;
    }
    index = i;
    localStorage.setItem('bazaarLinkIndex', index);
    counter.textContent = `${index + 1} / ${links.length}`;
    counter.appendChild(cogIcon);
    window.location.href = links[index];
};

    nextBtn.addEventListener('click', () => openLinkAtIndex((index + 1) % links.length));
    backToFirstBtn.addEventListener('click', () => openLinkAtIndex(0));

    // Settings management
    const saveSettings = () => {
        localStorage.setItem('bazaarNavSettings', JSON.stringify(settings));
        updateStyles();
    };

    const resetToDefaults = () => {
        Object.assign(settings, defaultSettings);
        saveSettings();
        populateSettingsPanel();
    };

    // Settings panel
    const populateSettingsPanel = () => {
        settingsPanel.innerHTML = `
            <div class="section">
                <div class="section-title">Presets</div>
                <button id="defaultPreset" style="width:100%;margin:4px 0;padding:4px">Default</button>
                <button id="darkPreset" style="width:100%;margin:4px 0;padding:4px">Dark Theme</button>
                <button id="lightPreset" style="width:100%;margin:4px 0;padding:4px">Light Theme</button>
                <button id="resetDefaults" style="width:100%;margin:4px 0;padding:4px">Reset All to Defaults</button>
            </div>

            <div class="section">
                <div class="section-title">Container</div>
                <label for="widthInput">Width:</label>
                <input type="range" id="widthInput" min="120" max="400" step="1" value="${settings.width}">

                <label for="opacityInput">Opacity:</label>
                <input type="range" id="opacityInput" min="0.5" max="1" step="0.05" value="${settings.opacity}">

                <label for="gradientStart">Gradient Start:</label>
                <input type="color" id="gradientStart" value="${settings.gradientStart}">

                <label for="gradientEnd">Gradient End:</label>
                <input type="color" id="gradientEnd" value="${settings.gradientEnd}">

                <label for="borderRadius">Border Radius:</label>
                <input type="number" id="borderRadius" min="0" max="50" value="${settings.borderRadius}">

                <label for="borderWidth">Border Width:</label>
                <input type="number" id="borderWidth" min="0" max="10" value="${settings.borderWidth}">

                <label for="borderColor">Border Color:</label>
                <input type="color" id="borderColor" value="${settings.borderColor}">

                <label for="shadowColor">Shadow Color:</label>
                <input type="color" id="shadowColor" value="${settings.shadowColor}">

                <label for="shadowBlur">Shadow Blur:</label>
                <input type="number" id="shadowBlur" min="0" max="20" value="${settings.shadowBlur}">
            </div>

            <div class="section">
                <div class="section-title">Text</div>
                <label for="textColor">Text Color:</label>
                <input type="color" id="textColor" value="${settings.textColor}">

                <label for="fontFamily">Font Family:</label>
                <select id="fontFamily">
                    <option value="Arial, sans-serif">Arial</option>
                    <option value="'Segoe UI', Tahoma, Geneva, Verdana, sans-serif">Segoe UI</option>
                    <option value="'Courier New', Courier, monospace">Courier New</option>
                    <option value="'Georgia', serif">Georgia</option>
                    <option value="'Comic Sans MS', cursive, sans-serif">Comic Sans MS</option>
                    <option value="'Trebuchet MS', sans-serif">Trebuchet MS</option>
                </select>

                <label for="fontSize">Font Size:</label>
                <input type="number" id="fontSize" min="10" max="20" value="${settings.fontSize}">
            </div>

            <div class="section">
                <div class="section-title">Buttons</div>
                <label for="buttonBgColor">Background:</label>
                <input type="color" id="buttonBgColor" value="${settings.buttonBgColor}">

                <label for="buttonTextColor">Text Color:</label>
                <input type="color" id="buttonTextColor" value="${settings.buttonTextColor}">

                <label for="buttonHoverColor">Hover Color:</label>
                <input type="color" id="buttonHoverColor" value="${settings.buttonHoverColor}">

                <label for="buttonBorderColor">Border Color:</label>
                <input type="color" id="buttonBorderColor" value="${settings.buttonBorderColor}">

                <label for="buttonBorderWidth">Border Width:</label>
                <input type="number" id="buttonBorderWidth" min="0" max="5" value="${settings.buttonBorderWidth}">
            </div>

            <div class="section">
                <div class="section-title">Counter</div>
                <label for="counterBgColor">Background:</label>
                <input type="color" id="counterBgColor" value="${settings.counterBgColor}">

                <label for="counterTextColor">Text Color:</label>
                <input type="color" id="counterTextColor" value="${settings.counterTextColor}">

                <label for="counterBorderColor">Border Color:</label>
                <input type="color" id="counterBorderColor" value="${settings.counterBorderColor}">

                <label for="loadingBarColor">Loading Bar Color:</label>
                <input type="color" id="loadingBarColor" value="${settings.loadingBarColor}">
            </div>

            <div class="section">
                <div class="section-title">Settings Panel</div>
                <label for="panelBgColor">Background:</label>
                <input type="color" id="panelBgColor" value="${settings.panelBgColor}">

                <label for="panelTextColor">Text Color:</label>
                <input type="color" id="panelTextColor" value="${settings.panelTextColor}">

                <label for="panelBorderColor">Border Color:</label>
                <input type="color" id="panelBorderColor" value="${settings.panelBorderColor}">
            </div>
        `;

        settingsPanel.querySelectorAll('input, select').forEach(el => {
    el.addEventListener('input', () => {
        if (el.id === 'widthInput') {
            settings.width = parseInt(el.value);
            container.style.width = `${settings.width}px`;
        } else if (el.id === 'opacityInput') {
            settings.opacity = parseFloat(el.value);
            container.style.opacity = settings.opacity;
        } else if (el.type === 'color') {
            settings[el.id] = el.value;
        } else if (el.type === 'number' || el.type === 'range') {
            settings[el.id] = parseInt(el.value);
        } else {
            settings[el.id] = el.value;
        }
        saveSettings();
    });
});

        // Preset buttons
        settingsPanel.querySelector('#defaultPreset').addEventListener('click', () => {
            Object.assign(settings, defaultSettings);
            saveSettings();
            populateSettingsPanel();
        });

        settingsPanel.querySelector('#darkPreset').addEventListener('click', () => {
            Object.assign(settings, {
                gradientStart: '#2d2d2d', gradientEnd: '#1a1a1a',
                textColor: '#e0e0e0', fontFamily: 'Arial, sans-serif',
                buttonBgColor: '#444444', buttonTextColor: '#000000',
                buttonHoverColor: '#666666', counterBgColor: '#333333',
                counterTextColor: '#ffffff', panelBgColor: '#222222',
                panelTextColor: '#dddddd', borderColor: '#444444',
                panelBorderColor: '#444444', counterBorderColor: '#555555',
                buttonBorderColor: '#666666', loadingBarColor: '#4a90e2'
            });
            saveSettings();
            populateSettingsPanel();
        });

        settingsPanel.querySelector('#lightPreset').addEventListener('click', () => {
            Object.assign(settings, {
                gradientStart: '#f5f5f5', gradientEnd: '#e0e0e0',
                textColor: '#000000', fontFamily: 'Arial, sans-serif',
                buttonBgColor: '#ffffff', buttonTextColor: '#000000',
                buttonHoverColor: '#8F4729', counterBgColor: '#f0f0f0',
                counterTextColor: '#000000', panelBgColor: '#ffffff',
                panelTextColor: '#000000',
                borderColor: '#000000',      // Border color
                borderWidth: 2,              // Border thickness (in pixels)
                panelBorderColor: '#000000',
                counterBorderColor: '#000000',
                buttonBorderColor: '#000000',
                buttonBorderWidth: 2,        // Button border thickness
                loadingBarColor: '#8F4729'
            });
            saveSettings();
            populateSettingsPanel();
        });

        settingsPanel.querySelector('#resetDefaults').addEventListener('click', resetToDefaults);
    };

    populateSettingsPanel();

    // Toggle settings panel
    cogIcon.addEventListener('click', () => settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block');
    cogIcon.addEventListener('keydown', e => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), cogIcon.click()));
})();