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}