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(row, statsCard.querySelector('.mb-1:has(.fa-calendar)')!);
48};
49
50/**
51 * 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.
52 * @param userId The ID of the user.
53 */
54export async function outfitCost(userId: number) {
55 const calculateBtn = document.createElement('small');
56 calculateBtn.classList.add('fw-normal');
57 calculateBtn.style.letterSpacing = '0px';
58
59 const calculate = async function() {
60 const outfit = {
61 cost: 0,
62 collectibles: 0,
63 offsale: 0,
64 timed: 0
65 };
66
67 const avatar: apiTypes.avatarApiSchema | "disabled" = await pullKVCache(
68 'avatars',
69 userId.toString(),
70 async () => {
71 if (!config.api.enabled) return "disabled";
72 return (await (await fetch(config.api.urls.public + "users/" + userId + "/avatar")).json());
73 },
74 30000, // 30 seconds
75 false
76 );
77
78 if (avatar == "disabled") {
79 calculateBtn.innerHTML = '<span class="text-secondary">Outfit cost unavailable, please try again later</span>';
80 throw new Error('[Poly+] API is disabled, cancelling avatar cost loading..');
81 };
82
83 if (avatar.isDefault) {
84 calculateBtn.innerHTML = '<span class="text-secondary">Default avatar</span>';
85 console.warn('[Poly+] User has default avatar, cancelling avatar cost loading..');
86 return;
87 };
88
89 for (const asset of avatar.assets.filter((asset) => asset.type != "profileTheme")) {
90 // ? API won't be disabled at this step, since there was already an API check previously
91 const item: apiTypes.itemApiSchema = await pullKVCache(
92 'items',
93 asset.id.toString(),
94 async () => {
95 return (await (await fetch(config.api.urls.public + "store/" + asset.id)).json());
96 },
97 600000,
98 false
99 );
100
101 if (item.isLimited) {
102 outfit.collectibles++;
103 outfit.cost += item.averagePrice!;
104 } else if (!item.price) {
105 outfit.offsale++;
106 } else if (item.onSaleUntil != null) {
107 outfit.timed++;
108 } else {
109 outfit.cost += item.price!;
110 };
111 };
112
113 console.log('[Poly+] Outfit breakdown: ', outfit);
114 calculateBtn.innerHTML = `<span class="text-success">${ outfit.cost.toLocaleString() }</span>`;
115 };
116
117 if (config.api.enabled) {
118 calculateBtn.innerHTML = '<a class="text-decoration-underline text-success" style="text-decoration-color: rgb(15, 132, 79) !important;">$ calculate avatar cost</a>';
119 calculateBtn.children[0].addEventListener('click', calculate);
120 } else {
121 calculateBtn.innerHTML = '<span class="text-secondary">Outfit cost unavailable, please try again later</span>';
122 console.error("[Poly+] API is disabled, outfit cost calculation is unavailable.")
123 }
124 document.querySelector('.section-title:first-child')!.appendChild(calculateBtn);
125};