Bazaar Directory - WTF is in there?

The new bazaar directory feature doesn't tell you anything about what is in each bazaar. This replaces the (largely) useless counter showing how many favorites a bazaar has with a button to show their bazaar's contents instead.

当前为 2025-05-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Bazaar Directory - WTF is in there?
// @namespace   Violentmonkey Scripts
// @match       https://www.torn.com/page.php*
// @grant       GM_xmlhttpRequest
// @version     1.0.1
// @author      Titanic_
// @license     MIT
// @description The new bazaar directory feature doesn't tell you anything about what is in each bazaar. This replaces the (largely) useless counter showing how many favorites a bazaar has with a button to show their bazaar's contents instead.
// ==/UserScript==

let userApiKey = getData("API_KEY") || "";

const bazaarIconSVG = `<svg class="bazaar-icon" style="scale: 0.75;"></svg>`;
window.MyCustomBazaarInterval = null;

async function fetchApi(endpoint, selections = "basic", apiKeyToUse = userApiKey) {
	if (!apiKeyToUse) {
		console.warn("API Key not set. Cannot fetch API.");
		return Promise.resolve({ error: { code: 0, error: "API Key not set" } });
	}

	return new Promise((resolve) => {
		GM_xmlhttpRequest({
			method: "GET",
			url: `https://api.torn.com/${endpoint}?key=${apiKeyToUse}&selections=${selections}`,
			timeout: 15000,
			onload: function (response) {
				let parsedJson;
				try {
					parsedJson = JSON.parse(response.responseText);
				} catch (e) {
					console.error(`Error parsing JSON response:`, e, "Response:", response.responseText);
					resolve({ error: { error: "JSON Parse Error", details: e.message, responseText: response.responseText } });
					return;
				}

				if (parsedJson?.error) {
					const errorMessage = parsedJson.error.error || JSON.stringify(parsedJson.error);
					if (parsedJson.error.error !== "API Key not set") console.error(`API Error (Status: ${response.status}): ${errorMessage}`);
					resolve(parsedJson);
					return;
				}

				if (response.status >= 200 && response.status < 300) resolve(parsedJson);
				else {
					console.error(`HTTP Error ${response.status}: Non-success status without specific API error in JSON.`, "Response:", response.responseText);
					resolve({
						error: {
							error: `HTTP Error ${response.status}`,
							details: "Server returned non-2xx status without a Torn API error object in JSON.",
							responseText: response.responseText,
						},
					});
				}
			},
			onerror: function (response) {
				console.error("Network Error:", response.statusText || "Unknown network issue", response);
				resolve({ error: { error: "Network Error", details: response.statusText || "Unknown network issue" } });
			},
			ontimeout: function () {
				console.error("Request Timeout");
				resolve({ error: { error: "Request Timeout" } });
			},
		});
	});
}

function checkUrl() {
	if (!window.location.href.includes("page.php?sid=bazaar")) {
		if (window.MyCustomBazaarInterval) {
			clearInterval(window.MyCustomBazaarInterval);
			window.MyCustomBazaarInterval = null;
		}
		return;
	}

	addBazaarIcons();
}

function addBazaarIcons() {
	document.querySelectorAll("li[class^=bazaarWrap]").forEach((row) => {
		const linkEl = row.querySelector("a[href*='bazaar.php']");
		if (!linkEl) return;

		const statsWrap = linkEl.querySelector("div[class^=statsWrap]");
		if (statsWrap) statsWrap.remove();

		if (!linkEl.querySelector(".bazaar-icon-container")) {
			const bazaarIcon = Object.assign(document.createElement("div"), {
				className: "bazaar-icon-container",
				innerHTML: bazaarIconSVG,
				style: "cursor: pointer; float: right; padding-left: 8px;",
			});

			linkEl.append(bazaarIcon);

			if (!bazaarIcon.dataset.listenerAttached) {
				bazaarIcon.addEventListener("click", (e) => {
					e.preventDefault();
					e.stopPropagation();
					toggleExpand(row);
				});
				bazaarIcon.dataset.listenerAttached = "true";
			}
		}
	});
}

function toggleExpand(row) {
	const existingDetailsDiv = row.querySelector(".expanded-bazaar-details");

	document.querySelectorAll(".expanded-bazaar-details").forEach((div) => {
		if (div.parentElement !== row) div.style.display = "none";
	});

	if (existingDetailsDiv) existingDetailsDiv.style.display = existingDetailsDiv.style.display === "none" ? "block" : "none";
	else {
		const detailsDiv = Object.assign(document.createElement("div"), {
			className: "expanded-bazaar-details",
			style: "",
		});

		const filterInput = Object.assign(document.createElement("input"), {
			type: "text",
			placeholder: "Filter item name",
			style: "width: calc(100% + 10px); text-align: center; background-color: #333333; color: #e0e0e0 !important; border: 1px outset #4f4f4f; padding: 3px;",
		});
		filterInput.addEventListener("input", () => {
			filterTable(table, filterInput.value);
		});
		detailsDiv.appendChild(filterInput);

		const table = Object.assign(document.createElement("table"), {
			style: "width: 100%; border-collapse: collapse; background-color: #383838;",
		});

		const headerRow = table.createTHead().insertRow();
		const columnHeaders = ["Name", "#", "$"];
		columnHeaders.forEach((header) => {
			headerRow.appendChild(
				Object.assign(document.createElement("th"), {
					textContent: header,
					style: `border: 1px solid #4F4F4F; padding: 5px; text-align: ${header == "Name" ? "left" : "right"}; background-color: #454545; color: #e0e0e0;`,
				})
			);
		});

		const placeholderCell = table.createTBody().insertRow().insertCell();
		Object.assign(placeholderCell, {
			colSpan: columnHeaders.length,
			textContent: "Details will be loaded here.",
			style: "text-align: center; padding: 3px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;",
		});

		detailsDiv.appendChild(table);
		row.appendChild(detailsDiv);
		detailsDiv.style.display = "block";

		populateBazaar(row, table);
	}
}

function filterTable(table, searchText) {
	Array.from(table.querySelector("tbody").querySelectorAll("tr")).forEach((row) => {
		const nameCell = row.querySelector("td:first-child");
		if (nameCell) {
			const name = nameCell.textContent.toLowerCase();
			if (name.includes(searchText.toLowerCase())) row.style.display = "";
			else row.style.display = "none";
		}
	});
}

async function populateBazaar(row, table) {
	const url = row.querySelector("a[href*='bazaar.php']").href;
	const userID = new URL(url).searchParams.get("userId");
	const data = await fetchApi(`user/${userID}`, "bazaar");

	const tbody = table.querySelector("tbody");
	tbody.innerHTML = "";

	if (data.error) {
		const errorRow = tbody.insertRow();
		const errorCell = errorRow.insertCell();
		errorCell.colSpan = 3;
		errorCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";

		if (data.error.error === "API Key not set") {
			const setKeyLink = Object.assign(document.createElement("a"), {
				href: "#",
				textContent: "Click to set Public API",
				style: "color: #88C9F2; cursor: pointer;",
			});

			setKeyLink.onclick = async (e) => {
				e.preventDefault();
				const newApiKeyInput = prompt("Please enter your Torn API (Public) key:");
				if (newApiKeyInput) {
					const trimmedKey = newApiKeyInput.trim();
					if (trimmedKey !== "") {
						setData("API_KEY", trimmedKey);
						userApiKey = trimmedKey;

						tbody.innerHTML = "";
						const loadingRow = tbody.insertRow();
						const loadingCell = loadingRow.insertCell();
						loadingCell.colSpan = 3;
						loadingCell.textContent = "Reloading bazaar data...";
						loadingCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";

						await populateBazaar(row, table);
					} else {
						alert("API Key cannot be empty.");
					}
				}
			};
			errorCell.innerHTML = "";
			errorCell.appendChild(setKeyLink);
		} else errorCell.textContent = `Error loading bazaar: ${data.error.error}`;
		return;
	}

	if (!data.bazaar || data.bazaar.length === 0) {
		const noItemsRow = tbody.insertRow();
		const noItemsCell = noItemsRow.insertCell();
		noItemsCell.colSpan = 3;
		noItemsCell.textContent = "No items available in this bazaar.";
		noItemsCell.style = "text-align: center; padding: 10px; font-style: italic; color: #a0a0a0; border: 1px solid #4F4F4F;";
		return;
	}

	const items = data.bazaar.map((item) => ({
		name: item.name,
		amount: item.quantity,
		price: item.price,
	}));

	const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));

	for (const item of sortedItems) {
		const itemRow = tbody.insertRow();

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: item.name,
				style:
					"border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px;",
			})
		);

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: item.amount.toLocaleString(),
				style: "border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: right;",
			})
		);

		itemRow.appendChild(
			Object.assign(document.createElement("td"), {
				textContent: "$" + item.price.toLocaleString(),
				style: "border: 1px solid #4F4F4F; padding: 5px; color: #E0E0E0 !important; text-align: right;",
			})
		);
	}

	const filterInput = row.querySelector('.expanded-bazaar-details > input[type="text"]');
	if (filterInput) {
		filterTable(table, filterInput.value);
	}
}

function getData(key) {
	return localStorage.getItem(key);
}

function setData(key, value) {
	localStorage.setItem(key, value);
}

function addStyle(css) {
	const styleEl = Object.assign(document.createElement("style"), { type: "text/css" });
	styleEl.appendChild(document.createTextNode(css));
	document.head.appendChild(styleEl);
}

if (window.MyCustomBazaarInterval) clearInterval(window.MyCustomBazaarInterval);
window.MyCustomBazaarInterval = setInterval(checkUrl, 1000);
checkUrl();

addStyle(`
    .bazaarWrap___XXYgz {
        flex-direction: column;
        height: fit-content !important;
    }
    .expanded-bazaar-details {
        display: block;
        max-width: 100%;
        width: 100%;
        max-height: 200px;
        overflow-y: scroll;
        overflow-x: hidden;
        background-color: #383838;
        border-top: 1px solid #222222;
        clear: both; color: #cccccc;
        padding-right: 10px;
    }
`);