Customizable Bazaar Filler

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

Verze ze dne 24. 02. 2025. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Customizable Bazaar Filler
// @namespace    http://tampermonkey.net/
// @version      1.36
// @description  On click, auto-fills bazaar item quantities and prices based on your preferences
// @match        https://www.torn.com/bazaar.php*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    const styleBlock = `
    /* Existing checkbox styling (visible square) */
    .item-toggle {
        width: 16px;
        height: 16px;
        border-radius: 3px;
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
        outline: none;
    }
    .item-toggle::after {
        content: '\\2713';
        position: absolute;
        font-size: 12px;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        display: none;
    }
    .item-toggle:checked::after {
        display: block;
    }
    /* Light mode */
    body:not(.dark-mode) .item-toggle {
        border: 1px solid #ccc;
        background: #fff;
    }
    body:not(.dark-mode) .item-toggle:checked {
        background: #007bff;
    }
    body:not(.dark-mode) .item-toggle:checked::after {
        color: #fff;
    }
    /* Dark mode */
    body.dark-mode .item-toggle {
        border: 1px solid #4e535a;
        background: #2f3237;
    }
    body.dark-mode .item-toggle:checked {
        background: #4e535a;
    }
    body.dark-mode .item-toggle:checked::after {
        color: #fff;
    }
    /* Checkbox wrapper to increase clickable area */
    .checkbox-wrapper {
        position: absolute;
        top: 50%;
        right: 10px;
        width: 26px;
        height: 26px;
        transform: translateY(-50%);
        cursor: pointer;
    }
    .checkbox-wrapper input.item-toggle {
        position: absolute;
        top: 5px;
        left: 5px;
    }

    /* Modal overlay */
    .settings-modal-overlay {
        position: fixed;
        top: 0; left: 0;
        width: 100%; height: 100%;
        background: rgba(0,0,0,0.5);
        z-index: 9999;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    /* Modal container */
    .settings-modal {
        background: #fff;
        padding: 20px;
        border-radius: 8px;
        min-width: 300px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.3);
        color: #000;
    }
    .settings-modal h2 {
        margin-top: 0;
    }
    .settings-modal label {
        display: block;
        margin: 10px 0 5px;
    }
    .settings-modal input, .settings-modal select {
        width: 100%;
        padding: 5px;
        box-sizing: border-box;
    }
    .settings-modal button {
        margin-top: 15px;
        padding: 5px 10px;
    }
    /* Button group alignment */
    .settings-modal div[style*="text-align:right"] {
        text-align: right;
    }
    /* Dark mode modal overrides */
    body.dark-mode .settings-modal {
        background: #2f3237;
        color: #fff;
        box-shadow: 0 2px 10px rgba(0,0,0,0.7);
    }
    body.dark-mode .settings-modal input,
    body.dark-mode .settings-modal select {
        background: #3c3f41;
        color: #fff;
        border: 1px solid #555;
    }
    body.dark-mode .settings-modal button {
        background: #555;
        color: #fff;
        border: none;
    }
    `;
    $('<style>').prop('type', 'text/css').html(styleBlock).appendTo('head');

    let apiKey = GM_getValue("tornApiKey", "");
    let pricingSource = GM_getValue("pricingSource", "Market Value");
    let itemMarketOffset = GM_getValue("itemMarketOffset", -1);
    let itemMarketMarginType = GM_getValue("itemMarketMarginType", "absolute");
    let itemMarketListing = GM_getValue("itemMarketListing", 1);
    let itemMarketClamp = GM_getValue("itemMarketClamp", false);
    let marketMarginOffset = GM_getValue("marketMarginOffset", 0);
    let marketMarginType = GM_getValue("marketMarginType", "absolute");

    const validPages = ["#/add", "#/manage"];
    let currentPage = window.location.hash;

    let itemMarketCache = {};

    function getItemIdByName(itemName) {
        const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
        for (let [id, info] of Object.entries(storedItems)) {
            if (info.name === itemName) return id;
        }
        return null;
    }

    function getPriceColor(listedPrice, marketValue) {
        if (marketValue <= 0) {
            return "#FFFFFF";
        }
        const ratio = listedPrice / marketValue;
        if (ratio < 0) return "#FF0000";
        if (ratio > 2) return "#008000";
        if (ratio < 1) {
            let t = Math.max(0, ratio);
            let r = Math.round(255 + (255 - 255) * t);
            let g = Math.round(0 + (255 - 0) * t);
            let b = Math.round(0 + (255 - 0) * t);
            return `rgb(${r},${g},${b})`;
        } else {
            let t = ratio - 1;
            let r = Math.round(255 + (0 - 255) * t);
            let g = Math.round(255 + (128 - 255) * t);
            let b = Math.round(255 + (0 - 255) * t);
            return `rgb(${r},${g},${b})`;
        }
    }

    async function fetchItemMarketData(itemId) {
        if (!apiKey) {
            console.error("No API key set for Item Market calls.");
            alert("No API key set. Please set your Torn API key in Bazaar Filler Settings before continuing.");
            return null;
        }
        const now = Date.now();
        if (itemMarketCache[itemId] && (now - itemMarketCache[itemId].time < 30000)) {
            return itemMarketCache[itemId].data;
        }
        const url = `https://api.torn.com/v2/market/${itemId}/itemmarket`;
        try {
            const res = await fetch(url, {
                headers: { 'Authorization': 'ApiKey ' + apiKey }
            });
            const data = await res.json();
            if (data.error) {
                console.error("Item Market API error:", data.error);
                alert("Item Market API error: " + data.error.error);
                return null;
            }
            itemMarketCache[itemId] = { time: now, data };
            return data;
        } catch (err) {
            console.error("Failed fetching Item Market data:", err);
            alert("Failed to fetch Item Market data. Check your API key or try again later.");
            return null;
        }
    }

async function updateAddRow($row, isChecked) {
    const $qtyInput = $row.find(".amount input").first();
    const $priceInput = $row.find(".price input").first();
    const $choiceCheckbox = $row.find("div.amount.choice-container input");

    if (!isChecked) {
        // If a tickable quantity checkbox exists and is checked, trigger one click to untick it.
        if ($choiceCheckbox.length && $choiceCheckbox.prop("checked")) {
            $choiceCheckbox.click();
        }
        // Reset quantity field.
        if ($qtyInput.data("orig") !== undefined) {
            $qtyInput.val($qtyInput.data("orig"));
            $qtyInput.removeData("orig");
        } else {
            $qtyInput.val("");
        }
        $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));

        // Reset price field.
        if ($priceInput.data("orig") !== undefined) {
            $priceInput.val($priceInput.data("orig"));
            $priceInput.removeData("orig");
            $priceInput.css("color", "");
        } else {
            $priceInput.val("");
        }
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
        return;
    }

    // Save original values if not already saved.
    if (!$qtyInput.data("orig")) $qtyInput.data("orig", $qtyInput.val());
    if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());

    const itemName = $row.find(".name-wrap span.t-overflow").text().trim();
    const itemId = getItemIdByName(itemName);
    const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
    const matchedItem = Object.values(storedItems).find(i => i.name === itemName);

    // Update quantity field.
    if ($choiceCheckbox.length) {
        // If not already checked, click once.
        if (!$choiceCheckbox.prop("checked")) {
            $choiceCheckbox.click();
        }
    } else {
        let qty = $row.find(".item-amount.qty").text().trim();
        $qtyInput.val(qty);
        $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
    }

    // Update price field based on pricing source.
    if (pricingSource === "Market Value" && matchedItem) {
        let mv = Number(matchedItem.market_value);
        let finalPrice = mv;
        if (marketMarginType === "absolute") {
            finalPrice += marketMarginOffset;
        } else if (marketMarginType === "percentage") {
            finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
        }
        $priceInput.val(finalPrice.toLocaleString());
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
        $priceInput.css("color", getPriceColor(finalPrice, mv));
    }
    else if (pricingSource === "Item Market" && itemId) {
        const data = await fetchItemMarketData(itemId);
        if (!data || !data.itemmarket?.listings?.length) return;
        let listings = data.itemmarket.listings;
        const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
        const listingsText = listings.slice(0, 5)
            .map((x, i) => `${i + 1}) $${x.price.toLocaleString()} x${x.amount}`)
            .join('\n');
        $checkbox.attr("title", listingsText);
        setTimeout(() => {
            $checkbox.removeAttr("title");
        }, 30000);
        let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
        let listingPrice = listings[baseIndex].price;
        let finalPrice;
        if (itemMarketMarginType === "absolute") {
            finalPrice = listingPrice + Number(itemMarketOffset);
        } else if (itemMarketMarginType === "percentage") {
            finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
        }
        if (itemMarketClamp && matchedItem && matchedItem.market_value) {
            finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
        }
        if (!$choiceCheckbox.length) {
            let qty = $row.find(".item-amount.qty").text().trim();
            $qtyInput.val(qty);
            $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
        } else {
            if (!$choiceCheckbox.prop("checked")) {
                $choiceCheckbox.click();
            }
        }
        $priceInput.val(finalPrice.toLocaleString());
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        $priceInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));
        if (matchedItem && matchedItem.market_value) {
            let marketVal = Number(matchedItem.market_value);
            $priceInput.css("color", getPriceColor(finalPrice, marketVal));
        }
    }
    else if (pricingSource === "Bazaars/TornPal") {
        alert("Bazaars/TornPal is not available. Please select another source.");
    }
}

async function updateManageRow($row, isChecked) {
    const $priceInput = $row.find(".price___DoKP7 .input-money-group.success input.input-money").first();
    if (!isChecked) {
        if ($priceInput.data("orig") !== undefined) {
            $priceInput.val($priceInput.data("orig"));
            $priceInput.removeData("orig");
            $priceInput.css("color", "");
        } else {
            $priceInput.val("");
        }

        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        return;
    }

    if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());

    const itemName = $row.find(".desc___VJSNQ b").text().trim();
    const itemId = getItemIdByName(itemName);
    const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
    const matchedItem = Object.values(storedItems).find(i => i.name === itemName);

    if (pricingSource === "Market Value" && matchedItem) {
        let mv = Number(matchedItem.market_value);
        let finalPrice = mv;
        if (marketMarginType === "absolute") {
            finalPrice += marketMarginOffset;
        } else if (marketMarginType === "percentage") {
            finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
        }
        $priceInput.val(finalPrice.toLocaleString());
        // Dispatch the input event to trigger listeners.
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        $priceInput.css("color", getPriceColor(finalPrice, mv));
    }
    else if (pricingSource === "Item Market" && itemId) {
        const data = await fetchItemMarketData(itemId);
        if (!data || !data.itemmarket?.listings?.length) return;
        let listings = data.itemmarket.listings;
        const $checkbox = $row.find(".checkbox-wrapper input.item-toggle").first();
        const listingsText = listings.slice(0, 5)
            .map((x, i) => `${i + 1}) $${x.price.toLocaleString()} x${x.amount}`)
            .join('\n');
        $checkbox.attr("title", listingsText);
        setTimeout(() => {
            $checkbox.removeAttr("title");
        }, 30000);
        let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
        let listingPrice = listings[baseIndex].price;
        let finalPrice;
        if (itemMarketMarginType === "absolute") {
            finalPrice = listingPrice + Number(itemMarketOffset);
        } else if (itemMarketMarginType === "percentage") {
            finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
        }
        if (itemMarketClamp && matchedItem && matchedItem.market_value) {
            finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
        }
        $priceInput.val(finalPrice.toLocaleString());
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        if (matchedItem && matchedItem.market_value) {
            let marketVal = Number(matchedItem.market_value);
            $priceInput.css("color", getPriceColor(finalPrice, marketVal));
        }
    }
    else if (pricingSource === "Bazaars/TornPal") {
        alert("Bazaars/TornPal is not available. Please select another source.");
    }
}


async function updateManageRowMobile($row, isChecked) {
    const $priceInput = $row.find("[class*=bottomMobileMenu___] [class*=priceMobile___] .input-money-group.success input.input-money").first();
    if (!$priceInput.length) {
        console.error("Mobile price field not found.");
        return;
    }
    if (!isChecked) {
        if ($priceInput.data("orig") !== undefined) {
            $priceInput.val($priceInput.data("orig"));
            $priceInput.removeData("orig");
            $priceInput.css("color", "");
        } else {
            $priceInput.val("");
        }
        // Dispatch input event to trigger key listeners.
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        return;
    }
    if (!$priceInput.data("orig")) $priceInput.data("orig", $priceInput.val());
    const itemName = $row.find(".desc___VJSNQ b").text().trim();
    const itemId = getItemIdByName(itemName);
    const storedItems = JSON.parse(localStorage.getItem("tornItems") || "{}");
    const matchedItem = Object.values(storedItems).find(i => i.name === itemName);
    if (pricingSource === "Market Value" && matchedItem) {
        let mv = Number(matchedItem.market_value);
        let finalPrice = mv;
        if (marketMarginType === "absolute") {
            finalPrice += marketMarginOffset;
        } else if (marketMarginType === "percentage") {
            finalPrice = Math.round(mv * (1 + marketMarginOffset / 100));
        }
        $priceInput.val(finalPrice.toLocaleString());
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        $priceInput.css("color", getPriceColor(finalPrice, mv));
    }
    else if (pricingSource === "Item Market" && itemId) {
        const data = await fetchItemMarketData(itemId);
        if (!data || !data.itemmarket?.listings?.length) return;
        let listings = data.itemmarket.listings;
        let baseIndex = Math.min(itemMarketListing - 1, listings.length - 1);
        let listingPrice = listings[baseIndex].price;
        let finalPrice;
        if (itemMarketMarginType === "absolute") {
            finalPrice = listingPrice + Number(itemMarketOffset);
        } else if (itemMarketMarginType === "percentage") {
            finalPrice = Math.round(listingPrice * (1 + Number(itemMarketOffset) / 100));
        }
        if (itemMarketClamp && matchedItem && matchedItem.market_value) {
            finalPrice = Math.max(finalPrice, Number(matchedItem.market_value));
        }
        $priceInput.val(finalPrice.toLocaleString());
        $priceInput[0].dispatchEvent(new Event("input", { bubbles: true }));
        if (matchedItem && matchedItem.market_value) {
            let marketVal = Number(matchedItem.market_value);
            $priceInput.css("color", getPriceColor(finalPrice, marketVal));
        }
    }
    else if (pricingSource === "Bazaars/TornPal") {
        alert("Bazaars/TornPal is not available. Please select another source.");
    }
}

    function openSettingsModal() {
        $('.settings-modal-overlay').remove();
        const $overlay = $('<div class="settings-modal-overlay"></div>');
        const $modal = $(`
            <div class="settings-modal" style="width:400px; max-width:90%; font-family:Arial, sans-serif;">
                <h2 style="margin-bottom:6px;">Bazaar Filler Settings</h2>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="margin-bottom:15px;">
                    <label for="api-key-input" style="font-weight:bold; display:block;">Torn API Key</label>
                    <input id="api-key-input" type="text" placeholder="Enter API key" style="width:100%; padding:6px; box-sizing:border-box;" value="${apiKey || ''}">
                </div>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="margin-bottom:15px;">
                    <label for="pricing-source-select" style="font-weight:bold; display:block;">Pricing Source</label>
                    <select id="pricing-source-select" style="width:100%; padding:6px; box-sizing:border-box;">
                        <option value="Market Value">Market Value</option>
                        <option value="Bazaars/TornPal">Bazaars/TornPal</option>
                        <option value="Item Market">Item Market</option>
                    </select>
                </div>
                <div id="market-value-options" style="display:none; margin-bottom:15px;">
                    <hr style="border-top:1px solid #ccc; margin:8px 0;">
                    <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Market Value Options</h3>
                    <div style="margin-bottom:10px;">
                        <label for="market-margin-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
                        <input id="market-margin-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${marketMarginOffset}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="market-margin-type" style="display:block;">Margin Type</label>
                        <select id="market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="absolute">Absolute ($)</option>
                            <option value="percentage">Percentage (%)</option>
                        </select>
                    </div>
                </div>
                <div id="item-market-options" style="display:none; margin-bottom:15px;">
                    <hr style="border-top:1px solid #ccc; margin:8px 0;">
                    <h3 style="margin:0 0 10px 0; font-size:1em; font-weight:bold;">Item Market Options</h3>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-listing" style="display:block;">Listing Index (1 = lowest, 2 = 2nd lowest, etc)</label>
                        <input id="item-market-listing" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketListing}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-offset" style="display:block;">Margin (ie: -1 is either $1 less or 1% less depending on margin type)</label>
                        <input id="item-market-offset" type="number" style="width:100%; padding:6px; box-sizing:border-box;" value="${itemMarketOffset}">
                    </div>
                    <div style="margin-bottom:10px;">
                        <label for="item-market-margin-type" style="display:block;">Margin Type</label>
                        <select id="item-market-margin-type" style="width:100%; padding:6px; box-sizing:border-box;">
                            <option value="absolute">Absolute ($)</option>
                            <option value="percentage">Percentage (%)</option>
                        </select>
                    </div>
                    <div style="display:inline-flex; align-items:center; margin-bottom:5px;">
                        <input id="item-market-clamp" type="checkbox" style="margin-right:5px;" ${itemMarketClamp ? "checked" : ""}>
                        <label for="item-market-clamp" style="margin:0; cursor:pointer;">Clamp minimum price to Market Value</label>
                    </div>
                </div>
                <hr style="border-top:1px solid #ccc; margin:8px 0;">
                <div style="text-align:right;">
                    <button id="settings-save" style="margin-right:8px; padding:6px 10px; cursor:pointer;">Save</button>
                    <button id="settings-cancel" style="padding:6px 10px; cursor:pointer;">Cancel</button>
                </div>
            </div>
        `);
        $overlay.append($modal);
        $('body').append($overlay);
        $('#pricing-source-select').val(pricingSource);
        $('#item-market-margin-type').val(itemMarketMarginType);
        $('#market-margin-type').val(marketMarginType);
        function toggleFields() {
            let src = $('#pricing-source-select').val();
            $('#item-market-options').toggle(src === 'Item Market');
            $('#market-value-options').toggle(src === 'Market Value');
        }
        $('#pricing-source-select').change(toggleFields);
        toggleFields();
        $('#settings-save').click(function() {
            apiKey = $('#api-key-input').val().trim();
            pricingSource = $('#pricing-source-select').val();
            if (pricingSource === "Bazaars/TornPal") {
                alert("Bazaars/TornPal is not available. Please select another source.");
                return;
            }
            if (pricingSource === "Market Value") {
                marketMarginOffset = Number($('#market-margin-offset').val() || 0);
                marketMarginType = $('#market-margin-type').val();
                GM_setValue("marketMarginOffset", marketMarginOffset);
                GM_setValue("marketMarginType", marketMarginType);
            }
            if (pricingSource === "Item Market") {
                itemMarketListing = Number($('#item-market-listing').val() || 1);
                itemMarketOffset = Number($('#item-market-offset').val() || -1);
                itemMarketMarginType = $('#item-market-margin-type').val();
                itemMarketClamp = $('#item-market-clamp').is(':checked');
                GM_setValue("itemMarketListing", itemMarketListing);
                GM_setValue("itemMarketOffset", itemMarketOffset);
                GM_setValue("itemMarketMarginType", itemMarketMarginType);
                GM_setValue("itemMarketClamp", itemMarketClamp);
            }
            GM_setValue("tornApiKey", apiKey);
            GM_setValue("pricingSource", pricingSource);
            $overlay.remove();
        });
        $('#settings-cancel').click(() => $overlay.remove());
    }

    function addPricingSourceLink() {
        if (document.getElementById('pricing-source-button')) return;
        let linksContainer = document.querySelector('.linksContainer___LiOTN');
        if (!linksContainer) return;
        let link = document.createElement('a');
        link.id = 'pricing-source-button';
        link.href = '#';
        link.className = 'linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP iconActive___oAum9';
        link.target = '_self';
        link.rel = 'noreferrer';
        const iconSpan = document.createElement('span');
        iconSpan.className = 'iconWrapper___x3ZLe iconWrapper___COKJD svgIcon___IwbJV';
        iconSpan.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
              <path d="M8 4.754a3.246 3.246 0 1 1 0 6.492 3.246 3.246 0 0 1 0-6.492zM5.754 8a2.246 2.246 0 1 0 4.492 0 2.246 2.246 0 0 0-4.492 0z"/>
              <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.433 2.54 2.54l.292-.16a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.433-.902 2.54-2.541l-.16-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.54-2.54l-.292.16a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.416 1.6.42 1.184 1.185l-.16.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.318.094a1.873 1.873 0 0 0-1.116 2.692l.16.292c.416.764-.42 1.6-1.185 1.184l-.291-.16a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.318a1.873 1.873 0 0 0-2.692-1.116l-.292.16c-.764.416-1.6-.42-1.184-1.185l.16-.292a1.873 1.873 0 0 0-1.116-2.692l-.318-.094c-.835-.246-.835-1.428 0-1.674l.318-.094a1.873 1.873 0 0 0 1.116-2.692l-.16-.292c-.416-.764.42-1.6 1.185-1.184l.292.16a1.873 1.873 0 0 0 2.693-1.116l.094-.318z"/>
            </svg>
        `;
        link.appendChild(iconSpan);
        const textSpan = document.createElement('span');
        textSpan.className = 'linkTitle____NPyM';
        textSpan.textContent = 'Bazaar Filler Settings';
        link.appendChild(textSpan);
        link.addEventListener('click', function(e) {
            e.preventDefault();
            openSettingsModal();
        });
        linksContainer.insertBefore(link, linksContainer.firstChild);
    }

    function addAddPageCheckboxes() {
        $(".items-cont .title-wrap").each(function() {
            if ($(this).find(".checkbox-wrapper").length) return;
            $(this).css("position", "relative");
            const wrapper = $('<div class="checkbox-wrapper"></div>');
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function(e) {
                    e.stopPropagation();
                    if (!GM_getValue("tornApiKey", "")) {
                        alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
                        $(this).prop("checked", false);
                        openSettingsModal();
                        return;
                    }
                    await updateAddRow($(this).closest("li.clearfix"), this.checked);
                }
            });
            wrapper.append(checkbox);
            $(this).append(wrapper);
        });
    }

    function addManagePageCheckboxes() {
        $(".item___jLJcf").each(function() {
            const $desc = $(this).find(".desc___VJSNQ");
            if (!$desc.length || $desc.find(".checkbox-wrapper").length) return;
            $desc.css("position", "relative");
            const wrapper = $('<div class="checkbox-wrapper"></div>');
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function(e) {
                    e.stopPropagation();
                    if (!GM_getValue("tornApiKey", "")) {
                        alert("No Torn API key set. Please click the 'Bazaar Filler Settings' button to enter your API key.");
                        $(this).prop("checked", false);
                        openSettingsModal();
                        return;
                    }
                    const $row = $(this).closest(".item___jLJcf");
                    if (window.innerWidth <= 784) {
                        const $manageBtn = $row.find('button[aria-label="Manage"]').first();
                        if ($manageBtn.length) {
                            if (!$manageBtn.find('span').hasClass('active___OTFsm')) {
                                $manageBtn.click();
                            }
                            setTimeout(async () => {
                                await updateManageRowMobile($row, this.checked);
                            }, 200);
                            return;
                        }
                    }
                    await updateManageRow($row, this.checked);
                }
            });
            wrapper.append(checkbox);
            $desc.append(wrapper);
        });
    }

    if (!validPages.includes(currentPage)) return;
    const storedItems = localStorage.getItem("tornItems");
    const lastUpdated = GM_getValue("lastUpdated", "");
    const todayUTC = new Date().toISOString().split('T')[0];

    if (apiKey && (!storedItems || lastUpdated !== todayUTC || new Date().getUTCHours() === 0)) {
        fetch(`https://api.torn.com/torn/?key=${apiKey}&selections=items`)
            .then(r => r.json())
            .then(data => {
                if (!data.items) {
                    console.error("Failed to fetch Torn items or no items found. Possibly invalid API key or rate limit.");
                    return;
                }
                let filtered = {};
                for (let [id, item] of Object.entries(data.items)) {
                    if (item.tradeable) {
                        filtered[id] = {
                            name: item.name,
                            market_value: item.market_value
                        };
                    }
                }
                localStorage.setItem("tornItems", JSON.stringify(filtered));
                GM_setValue("lastUpdated", todayUTC);
            })
            .catch(err => {
                console.error("Error fetching Torn items:", err);
            });
    }

    const domObserver = new MutationObserver(() => {
        if (window.location.hash === "#/add") {
            addAddPageCheckboxes();
        } else if (window.location.hash === "#/manage") {
            addManagePageCheckboxes();
        }
        addPricingSourceLink();
    });
    domObserver.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('hashchange', () => {
        currentPage = window.location.hash;
        if (currentPage === "#/add") {
            addAddPageCheckboxes();
        } else if (currentPage === "#/manage") {
            addManagePageCheckboxes();
        }
        addPricingSourceLink();
    });

    if (currentPage === "#/add") {
        addAddPageCheckboxes();
    } else if (currentPage === "#/manage") {
        addManagePageCheckboxes();
    }
    addPricingSourceLink();

    $(document).on("click", "button.undo___FTgvP", function(e) {
        e.preventDefault();
        $(".item___jLJcf .checkbox-wrapper input.item-toggle:checked").each(function() {
            $(this).prop("checked", false);
            const $row = $(this).closest(".item___jLJcf");
            updateManageRow($row, false);
        });
    });

    $(document).on("click", ".clear-action", function(e) {
        e.preventDefault();
        $("li.clearfix .checkbox-wrapper input.item-toggle:checked").each(function() {
            $(this).prop("checked", false);
            const $row = $(this).closest("li.clearfix");
            updateAddRow($row, false);
        });
    });
})();