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