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};