A rewrite of Poly+, my quality-of-life browser extension for Polytoria. Built entirely fresh using the WXT extension framework, Typescript, and with added better overall code quality.
extension
1import config from "@/utils/config.json"; 2import * as apiTypes from "@/utils/api/types"; 3import { getAPI } from "@/utils/api"; 4 5/** 6 * Adds a row to the user statistics card on the profile page allowing the user to quickly view & copy another user's ID. 7 * @param userId The ID of the user. 8 */ 9export async function displayId(userId: number) { 10 const statsCard = document.getElementById("user-stats-card"); 11 if (!statsCard) return; // ? Incase the user is blocked, which means the stats card won't be present 12 13 const row = document.createElement("div"); 14 row.classList.add("mb-1"); 15 row.innerHTML = ` 16 <b> 17 <i class="fa fa-hashtag text-center d-inline-block" style="width:1.3em"></i> 18 Player ID 19 </b> 20 <span class="float-end"> 21 ${userId} 22 <a id="copy" href="#copy"> 23 <i class="fad fa-copy" style="margin-left: 5px;"></i> 24 </a> 25 </span> 26 `; 27 28 const copyBtn = row.getElementsByTagName("a")[0]; 29 copyBtn.addEventListener("click", () => { 30 navigator.clipboard.writeText(userId as unknown as string) 31 .then(() => { 32 const icon: HTMLElement = copyBtn.children[0] as HTMLElement; 33 copyBtn.classList.add("text-success"); 34 icon.setAttribute("class", "fa-duotone fa-circle-check"); 35 icon.style.marginLeft = "3px"; 36 37 setTimeout(() => { 38 copyBtn.classList.remove("text-success"); 39 icon.setAttribute("class", "fad fa-copy"); 40 icon.style.marginLeft = "5px"; 41 }, 1500); 42 }) 43 .catch(() => { 44 alert("Failure to copy user ID to clipboard."); 45 }); 46 }); 47 48 statsCard.children[0].insertBefore( 49 row, 50 statsCard.querySelector(".mb-1:has(.fa-calendar)")!, 51 ); 52} 53 54/** 55 * Adds a button next to the "Avatar" card heading, which, on click, adds up all the items the user who owns this profile is wearing. 56 * @param userId The ID of the user. 57 */ 58export async function outfitCost(userId: number) { 59 const publicAPI = getAPI("public"); 60 61 const calculateBtn = document.createElement("small"); 62 calculateBtn.classList.add("fw-normal"); 63 calculateBtn.style.letterSpacing = "0px"; 64 65 const calculate = async function () { 66 const outfit = { 67 cost: 0, 68 collectibles: 0, 69 offsale: 0, 70 timed: 0, 71 }; 72 73 const avatar: apiTypes.avatarApiSchema | "disabled" = await pullKVCache( 74 "avatars", 75 userId.toString(), 76 async () => { 77 if (!publicAPI.enabled) return "disabled"; 78 return (await (await fetch( 79 publicAPI.url + "users/" + userId + "/avatar", 80 )).json()); 81 }, 82 30000, // 30 seconds 83 false, 84 ); 85 86 if (avatar == "disabled") { 87 calculateBtn.innerHTML = 88 '<span class="text-secondary">Outfit cost unavailable, please try again later</span>'; 89 throw new Error( 90 "[Poly+] API is disabled, cancelling avatar cost loading..", 91 ); 92 } 93 94 if (avatar.isDefault) { 95 calculateBtn.innerHTML = 96 '<span class="text-secondary">Default avatar</span>'; 97 console.warn( 98 "[Poly+] User has default avatar, cancelling avatar cost loading..", 99 ); 100 return; 101 } 102 103 for ( 104 const asset of avatar.assets.filter((asset) => 105 asset.type != "profileTheme" 106 ) 107 ) { 108 // ? API won't be disabled at this step, since there was already an API check previously 109 const item: apiTypes.itemApiSchema = await pullKVCache( 110 "items", 111 asset.id.toString(), 112 async () => { 113 return (await (await fetch( 114 publicAPI.url + "store/" + asset.id, 115 )).json()); 116 }, 117 600000, 118 false, 119 ); 120 121 if (item.isLimited) { 122 outfit.collectibles++; 123 outfit.cost += item.averagePrice!; 124 } else if (!item.price) { 125 outfit.offsale++; 126 } else if (item.onSaleUntil != null) { 127 outfit.timed++; 128 } else { 129 outfit.cost += item.price!; 130 } 131 } 132 133 console.log("[Poly+] Outfit breakdown: ", outfit); 134 calculateBtn.innerHTML = 135 `<span class="text-success"><i class="pi pi-brick me-2"></i> approx. ${outfit.cost.toLocaleString()} brick(s)</span>`; 136 }; 137 138 if (publicAPI.enabled) { 139 calculateBtn.innerHTML = 140 '<a class="text-decoration-underline text-success" style="text-decoration-color: rgb(15, 132, 79) !important;">$ calculate avatar cost</a>'; 141 calculateBtn.children[0].addEventListener("click", calculate); 142 } else { 143 calculateBtn.innerHTML = 144 '<span class="text-secondary">Outfit cost unavailable, please try again later</span>'; 145 console.error( 146 "[Poly+] API is disabled, outfit cost calculation is unavailable.", 147 ); 148 } 149 document.querySelector(".section-title:first-child")!.appendChild( 150 calculateBtn, 151 ); 152}