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.35
// @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, trigger a 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 there's a tickable checkbox, click it; otherwise, set the value.
    if ($choiceCheckbox.length) {
        $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 {
            $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);
        });
    });
})();

QingJ © 2025

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