Quickly sell unwanted items in your inventory to various shops
// ==UserScript==
// @name Junk Seller
// @namespace http://tampermonkey.net/
// @version 0.2.0
// @description Quickly sell unwanted items in your inventory to various shops
// @author DF__Lykos [4070584]
// @match https://www.torn.com/item.php*
// @match https://www.torn.com/shops.php*
// @match https://www.torn.com/bigalgunshop.php*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ==================== Junk List (from price_comparison.txt) ====================
const JUNK_LIST = [
{ id: 181, name: "Bottle of Champagne", type: "Alcohol", seller: "Bits 'n' Bobs", sellerLink: "https://www.torn.com/shops.php?step=bitsnbobs" },
{ id: 645, name: "Safety Boots", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 176, name: "Chain Mail", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 178, name: "Flak Jacket", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 640, name: "Kevlar Gloves", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 646, name: "Hiking Boots", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 50, name: "Outer Tactical Vest", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 49, name: "Full Body Armor", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 647, name: "Leather Helmet", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 32, name: "Leather Vest", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 650, name: "Leather Gloves", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 649, name: "Leather Boots", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 648, name: "Leather Pants", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 34, name: "Bulletproof Vest", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 643, name: "Construction Helmet", type: "Armor", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 517, name: "Weston Marlin 177", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 83, name: "Dart Rampager", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 78, name: "Edomondo NSX", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 523, name: "Mercia SLR", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 520, name: "Lolo 458", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 80, name: "Bavaria M5", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 89, name: "Edomondo ACD", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 79, name: "Echo Quadrato", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 95, name: "Oceania SS", type: "Car", seller: "Docks", sellerLink: "https://www.torn.com/shops.php?step=docks" },
{ id: 404, name: "Bandana", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 625, name: "Wetsuit", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 621, name: "Snorkel", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 107, name: "Trench Coat", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 430, name: "Coconut Bra", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 626, name: "Diving Gloves", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 414, name: "Proda Sunglasses", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 412, name: "Sports Shades", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 624, name: "Bikini", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 622, name: "Flippers", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 413, name: "Mountie Hat", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 623, name: "Speedo", type: "Clothing", seller: "TC Clothing", sellerLink: "https://www.torn.com/shops.php?step=clothes" },
{ id: 54, name: "Diamond Ring", type: "Jewelry", seller: "Jewelry Store", sellerLink: "https://www.torn.com/shops.php?step=jewelry" },
{ id: 227, name: "Spear", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 173, name: "Butterfly Knife", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 8, name: "Axe", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 224, name: "Swiss Army Knife", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 110, name: "Leather Bullwhip", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 1231, name: "Golf Club", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 401, name: "Lead Pipe", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 4, name: "Knuckle Dusters", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 3, name: "Crowbar", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 439, name: "Frying Pan", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 1, name: "Hammer", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 5, name: "Pen Knife", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 7, name: "Dagger", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 217, name: "Claymore Sword", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 236, name: "Kama", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 402, name: "Ice Pick", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 247, name: "Katana", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 11, name: "Samurai Sword", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 400, name: "Guandao", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 438, name: "Cricket Bat", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 9, name: "Scimitar", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 6, name: "Kitchen Knife", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 2, name: "Baseball Bat", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 10, name: "Chainsaw", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 111, name: "Ninja Claws", type: "Melee", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 1495, name: "Ambergris Lump", type: "Other", seller: "Jewelry Store", sellerLink: "https://www.torn.com/shops.php?step=jewelry" },
{ id: 411, name: "Model Space Ship", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1498, name: "Tiger Bone Powder", type: "Other", seller: "Nikeh Performance", sellerLink: "https://www.torn.com/shops.php?step=nikeh" },
{ id: 1497, name: "Uncut Diamonds", type: "Other", seller: "Jewelry Store", sellerLink: "https://www.torn.com/shops.php?step=jewelry" },
{ id: 1483, name: "Insulin", type: "Other", seller: "Pharmacy", sellerLink: "https://www.torn.com/shops.php?step=pharmacy" },
{ id: 434, name: "Yakitori Lantern", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 894, name: "Cosmetics Case", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1482, name: "Bearer Bond", type: "Other", seller: "Pawn Shop", sellerLink: "https://www.torn.com/shops.php?step=pawnshop" },
{ id: 275, name: "Jade Buddha", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1251, name: "Boat Engine", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 409, name: "Yucca Plant", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 896, name: "Subway Pass", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1211, name: "Fishing Rod", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 890, name: "Headphones", type: "Other", seller: "Super Store", sellerLink: "https://www.torn.com/shops.php?step=super" },
{ id: 410, name: "Fire Hydrant", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1345, name: "Prescription", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1240, name: "Perfume", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 436, name: "Snowboard", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1335, name: "Parking Permit", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1336, name: "Birth Certificate", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1086, name: "Driver's License", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 418, name: "Tailor's Dummy", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1341, name: "Concert Ticket", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1253, name: "Tractor Part", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 279, name: "Maneki Neko", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 1342, name: "Travel Visa", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1256, name: "Machine Part", type: "Other", seller: "Bits 'n' Bobs", sellerLink: "https://www.torn.com/shops.php?step=bitsnbobs" },
{ id: 358, name: "Raw Ivory", type: "Other", seller: "Pawn Shop", sellerLink: "https://www.torn.com/shops.php?step=pawnshop" },
{ id: 1491, name: "Ergotamine Ampoule", type: "Other", seller: "Pharmacy", sellerLink: "https://www.torn.com/shops.php?step=pharmacy" },
{ id: 1337, name: "Diploma", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1493, name: "Whale Meat", type: "Other", seller: "Nikeh Performance", sellerLink: "https://www.torn.com/shops.php?step=nikeh" },
{ id: 1496, name: "Natural Pearls", type: "Other", seller: "Jewelry Store", sellerLink: "https://www.torn.com/shops.php?step=jewelry" },
{ id: 1486, name: "Turtle Shell", type: "Other", seller: "Jewelry Store", sellerLink: "https://www.torn.com/shops.php?step=jewelry" },
{ id: 1494, name: "Pangolin Scales", type: "Other", seller: "Nikeh Performance", sellerLink: "https://www.torn.com/shops.php?step=nikeh" },
{ id: 1485, name: "Shark Fin", type: "Other", seller: "Nikeh Performance", sellerLink: "https://www.torn.com/shops.php?step=nikeh" },
{ id: 1492, name: "Counterfeit Manga", type: "Other", seller: "Pawn Shop", sellerLink: "https://www.torn.com/shops.php?step=pawnshop" },
{ id: 1343, name: "Passport", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1489, name: "Ephedrine Powder", type: "Other", seller: "Pharmacy", sellerLink: "https://www.torn.com/shops.php?step=pharmacy" },
{ id: 1490, name: "Safrole Oil", type: "Other", seller: "Pharmacy", sellerLink: "https://www.torn.com/shops.php?step=pharmacy" },
{ id: 1484, name: "Bear Gall", type: "Other", seller: "Nikeh Performance", sellerLink: "https://www.torn.com/shops.php?step=nikeh" },
{ id: 1349, name: "License Plate", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 1339, name: "Bank Check", type: "Other", seller: "Print Shop", sellerLink: "https://www.torn.com/shops.php?step=printstore" },
{ id: 406, name: "Afro Comb", type: "Other", seller: "Recycling Center", sellerLink: "https://www.torn.com/shops.php?step=recyclingcenter" },
{ id: 487, name: "Thompson", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 249, name: "SKS Carbine", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 26, name: "AK-47", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 231, name: "Heckler & Koch SL8", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 241, name: "Bushmaster Carbon 15", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 108, name: "9mm Uzi", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 252, name: "Ithaca 37", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 25, name: "P90", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 232, name: "SIG 550", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 30, name: "Steyr AUG", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 29, name: "M16 A2 Rifle", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 23, name: "Benelli M1 Tactical", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 22, name: "Sawed-Off Shotgun", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 27, name: "M4A1 Colt Carbine", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 174, name: "XM8 Rifle", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 28, name: "Benelli M4 Super", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 24, name: "MP5 Navy", type: "Primary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 613, name: "Harpoon", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 175, name: "Taser", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 486, name: "TMP", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 230, name: "Flare Gun", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 16, name: "USP", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 15, name: "Beretta M9", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 489, name: "Luger", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 13, name: "Raven MP25", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 485, name: "Skorpion", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 189, name: "S&W Revolver", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 244, name: "Blowgun", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 14, name: "Ruger 57", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 19, name: "Magnum", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 20, name: "Desert Eagle", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 18, name: "Fiveseven", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 218, name: "Crossbow", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 243, name: "Taurus", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 240, name: "Type 98 Anti Tank", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 248, name: "Qsz-92", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 12, name: "Glock 17", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 177, name: "Cobra Derringer", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 483, name: "MP5k", type: "Secondary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 257, name: "Throwing Knife", type: "Temporary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 242, name: "HEG", type: "Temporary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
{ id: 220, name: "Grenade", type: "Temporary", seller: "Big Al's Gun Shop", sellerLink: "https://www.torn.com/bigalgunshop.php" },
];
// Fast lookup: item id -> junk list entry
const JUNK_MAP = Object.fromEntries(JUNK_LIST.map(e => [e.id, e]));
// Our type names -> Torn API "cat" parameter values
// (Armor = "Defensive" in the Torn v2 API enum)
const TYPE_TO_API_CAT = {
Alcohol: 'Alcohol',
Armor: 'Defensive',
Car: 'Car',
Clothing: 'Clothing',
Jewelry: 'Jewelry',
Melee: 'Melee',
Other: 'Other',
Primary: 'Primary',
Secondary: 'Secondary',
Temporary: 'Temporary',
};
// Distinct API categories actually needed for our list
const API_CATS = [...new Set(
JUNK_LIST.map(e => TYPE_TO_API_CAT[e.type]).filter(Boolean)
)];
// ==================== Constants ====================
const STORAGE_KEY_API = 'sell_junk_api_key';
const STORAGE_KEY_SESSION = 'sell_junk_session';
const STORAGE_KEY_CONFIG = 'sell_junk_config';
const API_BASE = 'https://api.torn.com/v2/user/inventory';
// Categories excluded from junk by default (protect rank-war gear)
const DEFAULT_OFF_CATS = new Set(['Armor', 'Melee', 'Primary', 'Secondary']);
// ==================== Storage Helpers ====================
function getApiKey() { return localStorage.getItem(STORAGE_KEY_API) || ''; }
function setApiKey(k) { localStorage.setItem(STORAGE_KEY_API, k); }
function getSession() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY_SESSION)) || null; }
catch { return null; }
}
function setSession(s) { localStorage.setItem(STORAGE_KEY_SESSION, JSON.stringify(s)); }
function clearSession() { localStorage.removeItem(STORAGE_KEY_SESSION); }
// Config: { excludedIds: number[] } — ids of items NOT considered junk
function buildDefaultConfig() {
return { excludedIds: JUNK_LIST.filter(e => DEFAULT_OFF_CATS.has(e.type)).map(e => e.id) };
}
function getConfig() {
try {
const raw = localStorage.getItem(STORAGE_KEY_CONFIG);
if (raw) return JSON.parse(raw);
} catch {}
return buildDefaultConfig();
}
function saveConfig(cfg) { localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify(cfg)); }
// ==================== Inventory API ====================
async function fetchCat(key, cat) {
const now = Math.floor(Date.now() / 1000); // Unix timestamp — bypasses Torn's 1-hour cache
const url = `${API_BASE}?cat=${encodeURIComponent(cat)}&offset=0&limit=250` +
`×tamp=${now}` +
`&key=${encodeURIComponent(key)}&comment=SellJunk`;
const resp = await fetch(url);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data.error) throw new Error(`API error ${data.error.code}: ${data.error.error}`);
return data.inventory?.items ?? [];
}
async function fetchAllMatchingInventory(key) {
const results = await Promise.all(
API_CATS.map(cat =>
fetchCat(key, cat).catch(err => {
console.warn(`[Sell Junk] cat=${cat}:`, err);
return [];
})
)
);
// Flatten, deduplicate by id+uid, keep only non-excluded items in our junk list.
const excludedSet = new Set(getConfig().excludedIds);
const seen = new Set();
const matches = [];
for (const items of results) {
for (const inv of items) {
const key = `${inv.id}_${inv.uid ?? ''}`;
if (seen.has(key)) continue;
seen.add(key);
if (JUNK_MAP[inv.id] && !excludedSet.has(inv.id)) {
matches.push({ inv, entry: JUNK_MAP[inv.id] });
}
}
}
return matches;
}
// ==================== Loading Overlay ====================
function showLoading() {
removePanel();
const ov = document.createElement('div');
ov.id = 'sell-junk-panel-overlay';
ov.style.cssText = overlayStyle();
ov.innerHTML = `
<div style="${boxStyle()};padding:30px 44px;text-align:center;">
<div style="font-size:14px;margin-bottom:8px;">🔍 Checking inventory…</div>
<div style="font-size:12px;color:#666;">
Querying ${API_CATS.length} categories in parallel
</div>
</div>`;
document.body.appendChild(ov);
return ov;
}
// ==================== Results Panel ====================
function showResults(matches) {
removePanel();
// Group by shop, preserving insertion order
const byShop = new Map();
for (const { inv, entry } of matches) {
const shop = entry.seller || '(Unknown)';
if (!byShop.has(shop)) {
byShop.set(shop, { link: entry.sellerLink, items: [] });
}
byShop.get(shop).items.push({ id: entry.id, name: entry.name, qty: inv.amount });
}
const ov = document.createElement('div');
ov.id = 'sell-junk-panel-overlay';
ov.style.cssText = overlayStyle();
const panel = document.createElement('div');
panel.style.cssText = [
boxStyle(),
'width:540px',
'max-width:94vw',
'max-height:82vh',
'overflow-y:auto',
'padding:20px 22px',
].join(';');
/* ---- header ---- */
let html = `
<div style="display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid #383838;padding-bottom:10px;margin-bottom:14px;">
<span style="font-size:15px;font-weight:bold;">🗑️ Sell Junk</span>
<div style="display:flex;gap:6px;align-items:center;">
<button id="sj-config-btn" title="Configure which items are treated as junk"
style="${btnStyle('#1a3a5a','#7ab')};font-size:11px;padding:3px 9px;">
⚙ Junk List
</button>
<button id="sj-key-btn" title="Update API key"
style="${btnStyle('#333','#888')};font-size:11px;padding:3px 9px;">
⚙ Key
</button>
<button id="sj-close-btn"
style="${btnStyle('#444','#ccc')};padding:3px 10px;">✕</button>
</div>
</div>`;
/* ---- body ---- */
if (byShop.size === 0) {
html += `
<div style="text-align:center;padding:20px 0;">
<p style="color:#666;font-size:13px;margin:0 0 14px;">
No junk found in your inventory.
</p>
<p style="color:#555;font-size:12px;margin:0 0 14px;">
Weapons & armor are excluded by default to protect rank war gear.<br>
Open the junk list config to add or remove items.
</p>
<button id="sj-config-btn-empty"
style="${btnStyle('#1a3a5a','#7ab')};padding:7px 16px;">
⚙ Configure Junk List
</button>
</div>`;
} else {
for (const [shop, { items }] of byShop) {
html += `
<div style="margin-bottom:12px;background:#111;border:1px solid #2e2e2e;
border-radius:5px;padding:10px 12px;">
<div style="margin-bottom:7px;">
<span style="font-weight:bold;font-size:13px;color:#c8c8c8;">
🏪 ${shop}
</span>
</div>
<ul style="margin:0;padding:0 0 0 14px;font-size:12px;color:#999;
line-height:1.7;">
${items.map(i =>
`<li>${i.name} <span style="color:#5a9;font-size:11px;">×${i.qty}</span></li>`
).join('')}
</ul>
</div>`;
}
}
/* ---- footer ---- */
html += `
<div style="border-top:1px solid #383838;padding-top:12px;margin-top:4px;
display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:11px;color:#555;">
${matches.length} item type(s) · ${byShop.size} shop(s)
</span>
${byShop.size > 0 ? `
<button id="sj-go-shops-btn" style="${btnStyle('#1a6a30','#fff')}">
🛒 Go to Shops →
</button>` : ''}
</div>`;
panel.innerHTML = html;
ov.appendChild(panel);
document.body.appendChild(ov);
ov.addEventListener('click', e => { if (e.target === ov) removePanel(); });
panel.querySelector('#sj-close-btn').addEventListener('click', removePanel);
panel.querySelector('#sj-key-btn').addEventListener('click', () => {
removePanel();
showApiKeyModal();
});
panel.querySelector('#sj-config-btn')?.addEventListener('click', () => {
removePanel();
showConfigPanel();
});
panel.querySelector('#sj-config-btn-empty')?.addEventListener('click', () => {
removePanel();
showConfigPanel();
});
panel.querySelector('#sj-go-shops-btn')?.addEventListener('click', () => {
startSession(byShop);
});
}
// ==================== Select All Junk ====================
/**
* Iterates every <li data-item="…"> in .sell-items-list and, for each one
* whose item-ID is in the current session's junk list:
* • choice-container items (qty=1): clicks the label to tick the checkbox
* • input-money-group items (qty>1): fills the visible text input with
* data-money (max qty) and fires events so Torn's JS reacts
*
* Full console logging is included — open DevTools > Console and look for
* [Sell Junk] entries to diagnose any issues.
*/
function selectAllJunk(btn, _attempt) {
const attempt = _attempt || 1;
const LOG = (...a) => console.log('[Sell Junk]', ...a);
// ── 1. Read session ───────────────────────────────────────────────
const session = getSession();
if (!session) {
LOG('selectAllJunk: no active session in localStorage');
return;
}
const { shops, currentIndex } = session;
const current = shops[currentIndex];
LOG(`selectAllJunk: shop="${current.name}" items=`, current.items);
// ── 2. Build junk-ID set (handle stale sessions missing .id) ─────
// Fallback: look up by name via JUNK_MAP if id is absent
const junkIds = new Set();
for (const item of current.items) {
if (item.id !== undefined && item.id !== null) {
junkIds.add(String(item.id));
} else {
// Stale session — recover id from JUNK_MAP by name
const found = JUNK_LIST.find(e => e.name === item.name);
if (found) junkIds.add(String(found.id));
}
}
LOG('selectAllJunk: junkIds =', [...junkIds]);
// ── 3. Wait for sell list ─────────────────────────────────────────
const sellList = document.querySelector('.sell-items-list');
if (!sellList) {
LOG(`selectAllJunk: .sell-items-list not found (attempt ${attempt})`);
if (attempt <= 10) {
if (btn && attempt === 1) { btn.textContent = 'Waiting…'; btn.disabled = true; }
setTimeout(() => selectAllJunk(btn, attempt + 1), 500);
} else {
LOG('selectAllJunk: gave up waiting for .sell-items-list');
if (btn) { btn.textContent = '☑ Select All Junk'; btn.disabled = false; }
}
return;
}
const allLis = sellList.querySelectorAll('li[data-item]');
LOG(`selectAllJunk: found ${allLis.length} li[data-item] rows in sell list`);
let count = 0;
allLis.forEach(li => {
const dataItem = li.getAttribute('data-item');
if (!junkIds.has(dataItem)) {
LOG(` skip data-item="${dataItem}" (not in junk list)`);
return;
}
LOG(` process data-item="${dataItem}"`);
// ── Toggle (qty = 1): choice-container checkbox ───────────────
const checkbox = li.querySelector('.choice-container input[type="checkbox"]');
if (checkbox) {
LOG(` → checkbox found, checked=${checkbox.checked}`);
if (!checkbox.checked) {
const label = li.querySelector('label.marker-css');
if (label) {
LOG(' → clicking label');
label.click();
} else {
LOG(' → clicking checkbox directly');
checkbox.click();
}
}
count++;
return;
}
// ── Quantity input (qty > 1): input-money-group ───────────────
const textInput = li.querySelector('.input-money-group input[type="text"]');
if (textInput) {
const max = textInput.getAttribute('data-money') || '';
LOG(` → text input found, data-money="${max}", current value="${textInput.value}"`);
// Use native setter so React/framework change detection triggers
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
)?.set;
if (nativeSetter) {
nativeSetter.call(textInput, max);
} else {
textInput.value = max;
}
textInput.dispatchEvent(new Event('input', { bubbles: true }));
textInput.dispatchEvent(new Event('change', { bubbles: true }));
textInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
count++;
return;
}
LOG(` → no known control found in this li`);
});
LOG(`selectAllJunk: done — ${count} item(s) acted on`);
// Visual feedback
if (btn) {
const orig = '☑ Select All Junk';
btn.textContent = count > 0 ? `✔ ${count} selected` : '⚠ Nothing matched';
btn.disabled = true;
setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2500);
}
}
// ==================== Session: start & shop overlay ====================
function startSession(byShop) {
const shops = [];
for (const [name, { link, items }] of byShop) {
shops.push({ name, link, items });
}
setSession({ shops, currentIndex: 0 });
removePanel();
window.location.href = shops[0].link;
}
function showShopOverlay() {
const session = getSession();
if (!session || !session.shops) return;
const { shops, currentIndex } = session;
if (currentIndex >= shops.length) { clearSession(); return; }
const current = shops[currentIndex];
const isLast = currentIndex === shops.length - 1;
const total = shops.length;
// Remove any stale overlay from a previous inject attempt
document.getElementById('sell-junk-shop-overlay')?.remove();
const el = document.createElement('div');
el.id = 'sell-junk-shop-overlay';
el.style.cssText = [
'position:fixed',
'top:24px',
'right:24px',
'z-index:99999',
'width:300px',
boxStyle(),
'padding:14px 16px',
].join(';');
el.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid #383838;padding-bottom:8px;margin-bottom:10px;">
<span style="font-size:13px;font-weight:bold;">🗑️ Sell Junk Mode</span>
<span style="font-size:11px;color:#555;background:#222;padding:2px 7px;
border-radius:10px;">${currentIndex + 1} / ${total}</span>
</div>
<div style="font-size:13px;font-weight:bold;color:#c8c8c8;margin-bottom:8px;">
🏪 ${current.name}
</div>
<ul style="margin:0 0 10px;padding:0 0 0 14px;font-size:12px;color:#999;
line-height:1.8;">
${current.items.map(i =>
`<li>${i.name} <span style="color:#5a9;font-size:11px;">×${i.qty}</span></li>`
).join('')}
</ul>
<button id="sj-select-all"
style="${btnStyle('#5a4a00','#f0c040')};width:100%;margin-bottom:10px;
text-align:center;border:1px solid #7a6a00;">
☑ Select All Junk
</button>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button id="sj-mode-cancel"
style="${btnStyle('#444','#bbb')}">✕ Cancel</button>
<button id="sj-mode-next"
style="${btnStyle(isLast ? '#2a7a3a' : '#1a4fa0', '#fff')}">
${isLast ? '✓ Finish' : 'Next Shop →'}
</button>
</div>`;
document.body.appendChild(el);
el.querySelector('#sj-select-all').addEventListener('click', function () {
selectAllJunk(this);
});
el.querySelector('#sj-mode-cancel').addEventListener('click', () => {
clearSession();
el.remove();
});
el.querySelector('#sj-mode-next').addEventListener('click', () => {
if (isLast) {
clearSession();
el.remove();
} else {
setSession({ shops, currentIndex: currentIndex + 1 });
window.location.href = shops[currentIndex + 1].link;
}
});
}
// ==================== Config Panel ====================
function showConfigPanel() {
removePanel();
const cfg = getConfig();
const excludedSet = new Set(cfg.excludedIds);
// Group JUNK_LIST by category; sort: non-default-off first then alpha
const byCategory = new Map();
for (const item of JUNK_LIST) {
if (!byCategory.has(item.type)) byCategory.set(item.type, []);
byCategory.get(item.type).push(item);
}
const sortedCats = [...byCategory.keys()].sort((a, b) => {
const aOff = DEFAULT_OFF_CATS.has(a) ? 1 : 0, bOff = DEFAULT_OFF_CATS.has(b) ? 1 : 0;
return aOff !== bOff ? aOff - bOff : a.localeCompare(b);
});
const ov = document.createElement('div');
ov.id = 'sell-junk-panel-overlay';
ov.style.cssText = overlayStyle();
const panel = document.createElement('div');
panel.style.cssText = [
boxStyle(), 'width:560px', 'max-width:94vw',
'max-height:86vh', 'overflow-y:auto', 'padding:20px 22px',
].join(';');
const cbBase = 'width:14px;height:14px;margin-right:7px;cursor:pointer;' +
'accent-color:#4A90E2;flex-shrink:0;';
let html = `
<!-- header -->
<div style="display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid #383838;padding-bottom:10px;margin-bottom:12px;">
<span style="font-size:15px;font-weight:bold;">⚙ Configure Junk List</span>
<div style="display:flex;gap:6px;">
<button id="sj-cfg-reset" style="${btnStyle('#3a2000','#ca6')};font-size:11px;padding:3px 9px;"
title="Restore factory defaults (weapons & armor excluded)">
↺ Defaults
</button>
<button id="sj-cfg-close" style="${btnStyle('#444','#ccc')};padding:3px 10px;">✕</button>
</div>
</div>
<p style="font-size:12px;color:#666;margin:0 0 14px;line-height:1.6;">
✔ Checked items are treated as junk and will appear in scan results.<br>
🛡 <em>Weapons & armor are unchecked by default to protect rank war equipment.</em>
</p>`;
for (const cat of sortedCats) {
const items = byCategory.get(cat);
const isProtected = DEFAULT_OFF_CATS.has(cat);
// Category is "on" if at least one item in it is NOT excluded
const catCheckedCount = items.filter(i => !excludedSet.has(i.id)).length;
const catChecked = catCheckedCount === items.length ? 'checked' : '';
const catIndet = catCheckedCount > 0 && catCheckedCount < items.length;
html += `
<div style="margin-bottom:8px;border:1px solid #2e2e2e;border-radius:5px;overflow:hidden;">
<div style="display:flex;align-items:center;padding:6px 10px;background:#161616;">
<input type="checkbox" class="sj-cfg-cat-cb" data-cat="${cat}"
id="sj-cfg-cat-${cat}" ${catChecked} style="${cbBase}">
<label for="sj-cfg-cat-${cat}"
style="font-weight:bold;font-size:12px;color:#c8c8c8;cursor:pointer;flex:1;">
${cat}${isProtected ? ' <span style="color:#888;font-size:10px;">🛡 protected by default</span>' : ''}
</label>
<span style="font-size:11px;color:#555;">${items.length} item${items.length > 1 ? 's' : ''}</span>
</div>
<ul style="margin:0;padding:0;list-style:none;">
${items.map(item => {
const itemChecked = excludedSet.has(item.id) ? '' : 'checked';
return `
<li style="display:flex;align-items:center;padding:4px 10px 4px 26px;
border-top:1px solid #1e1e1e;">
<input type="checkbox" class="sj-cfg-item-cb" data-cat="${cat}"
data-id="${item.id}" id="sj-cfg-itm-${item.id}"
${itemChecked} style="${cbBase}">
<label for="sj-cfg-itm-${item.id}"
style="flex:1;font-size:12px;color:#999;cursor:pointer;">
${item.name}
</label>
<span style="font-size:11px;color:#555;">${item.seller}</span>
</li>`;
}).join('')}
</ul>
</div>`;
}
html += `
<div style="border-top:1px solid #383838;padding-top:12px;margin-top:4px;
display:flex;gap:8px;justify-content:flex-end;">
<button id="sj-cfg-cancel" style="${btnStyle('#444','#ccc')}">Cancel</button>
<button id="sj-cfg-save" style="${btnStyle('#1a4fa0','#fff')}">💾 Save & Rescan</button>
</div>`;
panel.innerHTML = html;
ov.appendChild(panel);
document.body.appendChild(ov);
// Fix indeterminate state (can't be set via HTML attribute)
panel.querySelectorAll('.sj-cfg-cat-cb').forEach(cb => {
const cat = cb.dataset.cat;
const items = [...panel.querySelectorAll(`.sj-cfg-item-cb[data-cat="${cat}"]`)];
const n = items.filter(i => i.checked).length;
cb.indeterminate = n > 0 && n < items.length;
});
/* ── helpers ── */
function syncCatCb(cat) {
const items = [...panel.querySelectorAll(`.sj-cfg-item-cb[data-cat="${cat}"]`)];
const n = items.filter(i => i.checked).length;
const catCb = panel.querySelector(`.sj-cfg-cat-cb[data-cat="${cat}"]`);
if (!catCb) return;
catCb.checked = n === items.length;
catCb.indeterminate = n > 0 && n < items.length;
}
function applyDefaults() {
panel.querySelectorAll('.sj-cfg-item-cb').forEach(cb => {
cb.checked = !DEFAULT_OFF_CATS.has(cb.dataset.cat);
});
panel.querySelectorAll('.sj-cfg-cat-cb').forEach(cb => {
cb.checked = !DEFAULT_OFF_CATS.has(cb.dataset.cat);
cb.indeterminate = false;
});
}
/* ── events ── */
panel.querySelectorAll('.sj-cfg-cat-cb').forEach(catCb =>
catCb.addEventListener('change', () => {
panel.querySelectorAll(`.sj-cfg-item-cb[data-cat="${catCb.dataset.cat}"]`)
.forEach(cb => { cb.checked = catCb.checked; });
catCb.indeterminate = false;
})
);
panel.querySelectorAll('.sj-cfg-item-cb').forEach(itemCb =>
itemCb.addEventListener('change', () => syncCatCb(itemCb.dataset.cat))
);
panel.querySelector('#sj-cfg-reset').addEventListener('click', applyDefaults);
panel.querySelector('#sj-cfg-cancel').addEventListener('click', removePanel);
panel.querySelector('#sj-cfg-save').addEventListener('click', () => {
// Build excludedIds from unchecked item checkboxes
const excludedIds = [...panel.querySelectorAll('.sj-cfg-item-cb:not(:checked)')]
.map(cb => Number(cb.dataset.id));
saveConfig({ excludedIds });
removePanel();
fetchAndShow(); // rescan with new config
});
ov.addEventListener('click', e => { if (e.target === ov) removePanel(); });
panel.querySelector('#sj-cfg-close').addEventListener('click', removePanel);
}
// ==================== Fetch & Display ====================
async function fetchAndShow() {
const key = getApiKey();
const loading = showLoading();
try {
const matches = await fetchAllMatchingInventory(key);
loading.remove();
showResults(matches);
} catch (err) {
loading.remove();
if (/API error/i.test(err.message)) {
showApiKeyModal(`⚠ ${err.message} — please re-enter your key.`);
} else {
console.error('[Sell Junk]', err);
alert(`[Sell Junk] Unexpected error:\n${err.message}`);
}
}
}
// ==================== API Key Modal ====================
function showApiKeyModal(errorMsg = '') {
removePanel();
const currentKey = getApiKey();
const masked = currentKey
? `${currentKey.slice(0, 6)}••••••••••${currentKey.slice(-4)}`
: '';
const ov = document.createElement('div');
ov.id = 'sell-junk-panel-overlay';
ov.style.cssText = overlayStyle();
const modal = document.createElement('div');
modal.style.cssText = [boxStyle(), 'width:440px', 'max-width:92vw', 'padding:22px 24px'].join(';');
modal.innerHTML = `
<div style="font-size:15px;font-weight:bold;margin-bottom:10px;
border-bottom:1px solid #383838;padding-bottom:10px;">
🗑️ Sell Junk — API Key Setup
</div>
<p style="font-size:12px;color:#888;margin:0 0 12px;">
A <strong>Limited Access</strong> key with <strong>Inventory</strong>
permission is required.<br>
Create one at
<a href="/preferences.php#tab=api" target="_blank"
style="color:#4A90E2;">Preferences → API Keys</a>.
</p>
${currentKey ? `
<p style="font-size:12px;color:#5a8;margin:0 0 10px;">
✔ Stored key:
<code style="background:#222;padding:2px 5px;border-radius:3px;
font-size:11px;">${masked}</code>
</p>` : ''}
${errorMsg ? `
<p style="font-size:12px;color:#e05555;margin:0 0 10px;">${errorMsg}</p>` : ''}
<label for="sj-api-input"
style="font-size:12px;display:block;margin-bottom:5px;">API Key:</label>
<input id="sj-api-input" type="text" autocomplete="off"
placeholder="Paste your Limited Access API key here"
value="${currentKey}"
style="width:100%;box-sizing:border-box;padding:8px 10px;
background:#111;color:#eee;border:1px solid #555;
border-radius:4px;font-size:13px;margin-bottom:10px;outline:none;" />
<div id="sj-modal-status"
style="font-size:12px;min-height:18px;margin-bottom:12px;"></div>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button id="sj-modal-cancel" style="${btnStyle('#555','#ccc')}">Cancel</button>
<button id="sj-modal-save" style="${btnStyle('#1a4fa0','#fff')}">
Save & Scan Inventory
</button>
</div>`;
ov.appendChild(modal);
document.body.appendChild(ov);
setTimeout(() => {
const inp = document.getElementById('sj-api-input');
if (inp) { inp.focus(); inp.select(); }
}, 50);
ov.addEventListener('click', e => { if (e.target === ov) removePanel(); });
modal.querySelector('#sj-modal-cancel').addEventListener('click', removePanel);
modal.querySelector('#sj-modal-save').addEventListener('click', async () => {
const key = document.getElementById('sj-api-input').value.trim();
const stat = document.getElementById('sj-modal-status');
const btn = modal.querySelector('#sj-modal-save');
if (!key) {
stat.style.color = '#e05555';
stat.textContent = '⚠ Please enter an API key.';
return;
}
stat.style.color = '#888';
stat.textContent = 'Validating…';
btn.disabled = true;
try {
// Quick validation: fetch a single small category via v2
const resp = await fetch(
`${API_BASE}?cat=Alcohol&offset=0&limit=1` +
`&key=${encodeURIComponent(key)}&comment=SellJunkValidate`
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data.error) throw new Error(`${data.error.code}: ${data.error.error}`);
setApiKey(key);
updateButtonIndicator();
stat.style.color = '#5a8';
stat.textContent = '✔ Key saved! Scanning inventory…';
setTimeout(() => {
removePanel();
fetchAndShow();
}, 900);
} catch (err) {
stat.style.color = '#e05555';
stat.textContent = `✗ Validation failed: ${err.message}`;
btn.disabled = false;
}
});
}
// ==================== Shared Style Helpers ====================
function overlayStyle() {
return [
'position:fixed', 'top:0', 'left:0', 'width:100%', 'height:100%',
'background:rgba(0,0,0,0.65)', 'z-index:99999',
'display:flex', 'align-items:center', 'justify-content:center',
].join(';');
}
function boxStyle() {
return [
'background:var(--default-bg-panel-color,#1a1a1a)',
'color:var(--default-color,#ddd)',
'border:1px solid var(--default-bg-panel-border-color,#3a3a3a)',
'border-radius:6px',
'box-shadow:0 6px 28px rgba(0,0,0,0.6)',
'font-family:inherit',
].join(';');
}
function btnStyle(bg, color) {
return `cursor:pointer;background:${bg};color:${color};border:none;` +
`border-radius:4px;padding:5px 12px;font-size:12px;font-family:inherit;`;
}
function removePanel() {
document.getElementById('sell-junk-panel-overlay')?.remove();
}
// ==================== Button Indicator ====================
function updateButtonIndicator() {
const dot = document.getElementById('sell-junk-key-dot');
if (dot) dot.style.display = getApiKey() ? 'inline' : 'none';
}
// ==================== Button Click ====================
function handleClick() {
if (!getApiKey()) {
showApiKeyModal();
} else {
// Always clear any previous session and re-fetch fresh inventory
clearSession();
document.getElementById('sell-junk-shop-overlay')?.remove();
fetchAndShow();
}
}
// ==================== Button Injection ====================
function injectButton() {
const linksBar = document.getElementById('top-page-links-list');
if (!linksBar || document.getElementById('sell-junk-btn')) return;
const btn = document.createElement('a');
btn.id = 'sell-junk-btn';
btn.setAttribute('role', 'button');
btn.setAttribute('aria-label', 'Sell Junk');
btn.className = 'events t-clear h c-pointer m-icon line-h24 right last';
btn.style.cssText = 'cursor:pointer;';
btn.innerHTML = `
<span class="icon-wrap svg-icon-wrap">
<span class="link-icon-svg events">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
width="16" height="16" style="vertical-align:middle;">
<path fill="#777"
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0
1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1
0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5
0 0 0 1 0z"/>
<path fill="#777"
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2
0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1
1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1
1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0
1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
</svg>
</span>
</span>
<span id="sell-junk-btn-text">Sell Junk</span><span
id="sell-junk-key-dot"
title="API key stored — click to scan inventory"
style="display:none;color:#4CAF50;margin-left:3px;font-size:10px;">●</span>`;
btn.addEventListener('click', handleClick);
// Mirror 冰娃.js: insert after last <a> in the bar
const links = linksBar.querySelectorAll('a');
const last = links.length ? links[links.length - 1] : null;
if (last) last.insertAdjacentElement('afterend', btn);
else linksBar.appendChild(btn);
updateButtonIndicator();
}
// ==================== Initialisation ====================
function waitAndInject(maxAttempts = 30, intervalMs = 400) {
let attempts = 0;
const timer = setInterval(() => {
attempts++;
if (document.getElementById('top-page-links-list')) {
clearInterval(timer);
injectButton();
} else if (attempts >= maxAttempts) {
clearInterval(timer);
console.warn('[Sell Junk] Could not find #top-page-links-list');
}
}, intervalMs);
}
// Show shop overlay on all matched pages when a session is running.
// Small delay so the page's own CSS variables are applied before rendering.
if (getSession()) {
setTimeout(showShopOverlay, 300);
}
// Inject the toolbar button only on item.php
if (window.location.pathname === '/item.php') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitAndInject);
} else {
waitAndInject();
}
}
})();