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