Customizable Bazaar Filler

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

目前為 2025-02-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Customizable Bazaar Filler
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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 = `
    .item-toggle {
        position: absolute;
        width: 16px;
        height: 16px;
        top: 50%;
        right: 10px;
        transform: translateY(-50%);
        cursor: pointer;
        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;
    }

    /* 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 = {};

    const inputEvent = new Event("input", { bubbles: true });
    const keyupEvent = new Event("keyup", { bubbles: true });

    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 r1 = 255, g1 = 0,   b1 = 0;
        let r2 = 255, g2 = 255, b2 = 255;
        let r = Math.round(r1 + (r2 - r1) * t);
        let g = Math.round(g1 + (g2 - g1) * t);
        let b = Math.round(b1 + (b2 - b1) * t);
        return `rgb(${r},${g},${b})`;
    } else {
        let t = ratio - 1; // 0..1
        let r1 = 255, g1 = 255, b1 = 255;
        let r2 = 0,   g2 = 128, b2 = 0;
        let r = Math.round(r1 + (r2 - r1) * t);
        let g = Math.round(g1 + (g2 - g1) * t);
        let b = Math.round(b1 + (b2 - b1) * 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 enter one in Settings first.");
            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();
    if (!isChecked) {
        // Reset fields
        if ($qtyInput.data("orig") !== undefined) {
            $qtyInput.val($qtyInput.data("orig"));
            $qtyInput.removeData("orig");
        } else {
            $qtyInput.val("");
        }
        $qtyInput[0].dispatchEvent(new Event("keyup", { bubbles: true }));

        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 (!$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);

    if (pricingSource === "Market Value" && matchedItem) {
        let qty = $row.find(".item-amount.qty").text().trim();
        $qtyInput.val(qty).trigger("keyup");
        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()).trigger("input");
        $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(".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));
        }

        $qtyInput.val($row.find(".item-amount.qty").text().trim()).trigger("keyup");
        $priceInput.val(finalPrice.toLocaleString()).trigger("input");

        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) {
        // Reset fields
        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()).trigger("input");
        $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(".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()).trigger("input");
        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);

    // Set initial selections
    $('#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 addFillButtonAddPage() {
        if (pricingSource !== "Bazaars/TornPal") return;
        if ($("#fill-checked-items").length) return;
        let clearAction = $(".items-footer .clear-action");
        if (!clearAction.length) return;

        let fillBtn = $('<span id="fill-checked-items" class="clear-action t-blue h c-pointer">Fill Checked Items (Disabled)</span>');
        clearAction.after(fillBtn);
        fillBtn.click(() => alert("Bazaars/TornPal is not available. Please select another source."));
        clearAction.off("click").on("click", () => $(".item-toggle").prop("checked", false));
    }
    function addUpdateButtonManagePage() {
        if (pricingSource !== "Bazaars/TornPal") return;
        if ($("#update-checked-items").length) return;
        let undoBtn = $(".confirmation___eWdQi .undo___FTgvP");
        if (!undoBtn.length) return;

        let updateBtn = $('<button id="update-checked-items" type="button" style="margin-left:10px;">Update Checked (Disabled)</button>');
        undoBtn.after(updateBtn);
        updateBtn.click(() => alert("Bazaars/TornPal is not available. Please select another source."));
    }

    function addAddPageCheckboxes() {
        $(".items-cont .title-wrap").each(function() {
            if ($(this).find(".item-toggle").length) return;
            $(this).css("position", "relative");
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function(e) {
                    e.stopPropagation();
                    await updateAddRow($(this).closest("li.clearfix"), this.checked);
                }
            });
            $(this).append(checkbox);
        });
    }

    function addManagePageCheckboxes() {
        $(".item___jLJcf").each(function() {
            const $desc = $(this).find(".desc___VJSNQ");
            if (!$desc.length || $desc.find(".item-toggle").length) return;
            $desc.css("position", "relative");
            const checkbox = $('<input>', {
                type: "checkbox",
                class: "item-toggle",
                click: async function(e) {
                    e.stopPropagation();
                    await updateManageRow($(this).closest(".item___jLJcf"), this.checked);
                }
            });
            $desc.append(checkbox);
        });
    }

    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();
            addFillButtonAddPage();
        } else if (window.location.hash === "#/manage") {
            addManagePageCheckboxes();
            addUpdateButtonManagePage();
        }
        addPricingSourceLink();
    });
    domObserver.observe(document.body, { childList: true, subtree: true });

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

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