您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
On click, auto-fills bazaar item quantities and prices based on your preferences
当前为
// ==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(); })();