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
at main 8.3 kB view raw
1import "@/public/css/specific.css"; 2import config from "@/utils/config.json"; 3 4import { bricksToCurrency } from "@/utils/utilities"; 5import { userDetails } from "@/utils/types"; 6import * as apiTypes from "@/utils/api/types"; 7import { getAPI } from "@/utils/api"; 8 9const itemID = window.location.pathname.split("/")[2]; 10 11/** 12 * Adds the locale real-life dollar value next to the amount of bricks an item costs in the text of the purchase button. 13 */ 14export function irlBrickPrice() { 15 try { 16 const purchaseBtn = document.querySelector( 17 'button[onclick^="buy"], button[data-price]', 18 )!; 19 const currency = bricksToCurrency( 20 parseInt(purchaseBtn.getAttribute("data-price")!), 21 "USD", 22 ); 23 24 if (currency) { 25 const spanTag = document.createElement("span"); 26 spanTag.classList.add("text-muted"); 27 spanTag.style.fontSize = "0.7rem"; 28 spanTag.style.fontWeight = "lighter"; 29 spanTag.innerText = ` (${currency})`; 30 purchaseBtn.appendChild(spanTag); 31 } 32 } catch (e) { 33 // The store purchase button has several different ways of being represented, this should 34 // only happen when the item is already owned 35 console.warn("[Poly+] Failure to find purchase button on page."); 36 } 37 38 const addPrice = function (item: HTMLElement) { 39 const price = item.getElementsByClassName("text-success")[0]; 40 const purchaseBtn = item.querySelector("button[data-listing-id]"); 41 42 if (price && purchaseBtn) { 43 const currency = bricksToCurrency( 44 parseInt(purchaseBtn.getAttribute("data-price")!), 45 "USD", 46 ); 47 48 if (currency) { 49 const spanTag = document.createElement("span"); 50 spanTag.classList.add("text-muted"); 51 spanTag.style.fontSize = "0.7rem"; 52 spanTag.style.fontWeight = "lighter"; 53 spanTag.innerText = ` (${currency})`; 54 price.appendChild(spanTag); 55 } 56 } 57 }; 58 59 const resellers = document.getElementById("resellers-container"); 60 if (resellers) { 61 for (const reseller of resellers.children) { 62 addPrice(reseller as HTMLElement); 63 } 64 65 const mutations = new MutationObserver((mutations) => { 66 for (const record of mutations) { 67 for (const node of record.addedNodes) { 68 addPrice(node as HTMLElement); 69 } 70 } 71 }); 72 73 mutations.observe(resellers, { childList: true }); 74 } 75} 76 77/** 78 * Adds a button below the item thumbnail preview allowing the user to preview their avatar with the item they are looking at equipped. 79 * @param user The public-facing details of the authenticated user. 80 */ 81export function tryOn(user: userDetails) { 82 const publicAPI = getAPI("public"); 83 84 const itemId = window.location.pathname.split("/")[2]; 85 const favoriteBtn = document.querySelector( 86 'button[onclick^="toggleFavorite"]', 87 )!; 88 89 const button = document.createElement("button"); 90 button.classList.add("btn", "btn-outline-primary", "btn-sm", "mt-2"); 91 button.style.width = "50px"; 92 button.innerHTML = '<img src="' + browser.runtime.getURL("/svgs/vial.svg") + 93 '" width="15" height="15">'; 94 95 const modal = document.createElement("dialog"); 96 modal.classList.add("polyplus-modal"); 97 Object.assign(modal.style, { 98 width: "450px", 99 height: "500px", 100 border: "1px solid #484848", 101 backgroundColor: "#181818", 102 borderRadius: "20px", 103 overflow: "hidden", 104 }); 105 106 modal.innerHTML = ` 107 <div class="row text-muted mb-2" style="font-size: 0.8rem;"> 108 <div class="col"> 109 <h5 class="mb-0" style="color: #fff;">Poly+ Item Preview</h5> 110 Try this item on your avatar before purchasing it! 111 </div> 112 <div class="col-md-2"> 113 <button class="btn btn-info w-100 mx-auto" onclick="this.parentElement.parentElement.parentElement.close();">X</button> 114 </div> 115 </div> 116 </div> 117 <div class="modal-body"></div> 118 `; 119 120 document.body.prepend(modal); 121 favoriteBtn.parentElement!.appendChild(button); 122 123 const loadFrame = async (source: apiTypes.avatarApiSchema) => { 124 console.info("[Poly+] Loading avatar preview with avatar data: ", source); 125 for (const [key, value] of Object.entries(source.colors)) { 126 source.colors[key as keyof typeof source.colors] = "#" + value; 127 } 128 129 const avatar: { [key: string]: any } = { 130 useCharacter: true, 131 items: [] as Array<number>, 132 headColor: source.colors.head, 133 torsoColor: source.colors.torso, 134 leftArmColor: source.colors.leftArm, 135 rightArmColor: source.colors.rightArm, 136 leftLegColor: source.colors.leftLeg, 137 rightLegColor: source.colors.rightLeg, 138 }; 139 140 const itemInfo: apiTypes.itemApiSchema = 141 await (await fetch(publicAPI.url + "store/" + itemId)).json(); 142 for ( 143 const item of [ 144 ...source.assets.filter(( 145 item: any, 146 ) => (item.type != itemInfo.type || itemInfo.type == "hat")), 147 { 148 id: itemInfo.id, 149 type: itemInfo.type, 150 }, 151 ] 152 ) { 153 if (item.type == "hat" || item.type == "tool") { 154 const mesh: apiTypes.meshApiSchema = await (await fetch( 155 publicAPI.url + "assets/serve-mesh/" + item.id, 156 )).json(); 157 if (mesh.success) { 158 if (item.type == "hat") { 159 avatar.items.push(mesh.url); 160 } else { 161 avatar.tool = mesh.url; 162 } 163 } 164 } else { 165 const texture: apiTypes.textureApiSchema = await (await fetch( 166 publicAPI.url + "assets/serve/" + item.id + "/Asset", 167 )).json(); 168 if (texture.success) avatar[item.type] = texture.url; 169 } 170 } 171 172 const frame = document.createElement("iframe"); 173 Object.assign(frame.style, { 174 width: "100%", 175 height: "auto", 176 aspectRatio: "1", 177 borderRadius: "20px", 178 background: "#1e1e1e", 179 }); 180 frame.src = `https://polytoria.com/ptstatic/itemview/#${ 181 btoa(encodeURIComponent(JSON.stringify(avatar))) 182 }`; 183 184 modal.getElementsByClassName("modal-body")[0].appendChild(frame); 185 }; 186 187 button.addEventListener("click", async () => { 188 if (!modal.getElementsByTagName("iframe")[0]) { 189 button.innerHTML = ` 190 <span class="spinner-grow spinner-grow-sm" aria-hidden="true"></span> 191 <span class="visually-hidden" role="status">Loading...</span> 192 `; 193 194 const avatar = await user.getAvatar(); 195 if (avatar == "disabled") { 196 console.error( 197 "[Poly+] API is disabled, cancelling try-on items loading..", 198 ); 199 200 const modalBody = modal.getElementsByClassName( 201 "modal-body", 202 )[0] as HTMLDivElement; 203 modalBody.style.marginTop = "50px"; 204 205 modalBody.innerHTML = ` 206 <div class="text-center p-2"> 207 <img src="${ 208 browser.runtime.getURL("/svgs/error.svg") 209 }" width="100" height="100"> 210 <p class="text-muted mb-0">Sorry! This feature is currently unavailable. Please check back later!</p> 211 </div> 212 `; 213 214 modal.showModal(); 215 button.innerHTML = '<img src="' + 216 browser.runtime.getURL("/svgs/vial.svg") + 217 '" width="15" height="15">'; 218 return; 219 } 220 221 await loadFrame(avatar); 222 button.innerHTML = '<img src="' + 223 browser.runtime.getURL("/svgs/vial.svg") + '" width="15" height="15">'; 224 } 225 226 modal.showModal(); 227 }); 228} 229 230/** 231 * Replaces the item sales counter with the number of owners the item has, useful for items that were granted by staff or earned via an event. 232 */ 233export async function accurateOwnerCount() { 234 const publicAPI = getAPI("public"); 235 236 const counter = document.querySelectorAll(".col.text-center")[2]!; 237 if (!counter || counter.children[1].textContent!.trim() != "0") return; 238 239 const owners: "disabled" | number = await pullKVCache( 240 "ownerCount", 241 itemID, 242 async () => { 243 if (!publicAPI.enabled) return "disabled"; 244 const res: apiTypes.ownersApiSchema = await (await fetch( 245 publicAPI.url + "store/" + itemID + "/owners", 246 )).json(); 247 return res.total; 248 }, 249 60000, // 1 minute 250 false, 251 ); 252 253 if (owners == "disabled") { 254 throw new Error( 255 "[Poly+] API disabled, cancelling accurate item owner count loading..", 256 ); 257 } 258 259 (counter.children[0] as HTMLHeadingElement).innerText = "Owners"; 260 (counter.children[1] as HTMLHeadingElement).innerText = owners 261 .toLocaleString(); 262}