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}