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 "@/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"; 7 8/** 9 * Adds the locale real-life dollar value next to the amount of bricks an item costs in the text of the purchase button. 10 */ 11export function irlBrickPrice() { 12 try { 13 const purchaseBtn = document.querySelector('button[onclick^="buy"], button[data-price]')!; 14 const currency = bricksToCurrency(parseInt(purchaseBtn.getAttribute('data-price')!), "USD"); 15 16 if (currency) { 17 const spanTag = document.createElement('span'); 18 spanTag.classList.add('text-muted'); 19 spanTag.style.fontSize = '0.7rem'; 20 spanTag.style.fontWeight = 'lighter'; 21 spanTag.innerText = ` (${currency})`; 22 purchaseBtn.appendChild(spanTag); 23 }; 24 } catch(e) { 25 // The store purchase button has several different ways of being represented, this should 26 // only happen when the item is already owned 27 console.warn('[Poly+] Failure to find purchase button on page.'); 28 }; 29 30 const addPrice = function(item: HTMLElement) { 31 const price = item.getElementsByClassName('text-success')[0]; 32 const purchaseBtn = item.querySelector('button[data-listing-id]'); 33 34 if (price && purchaseBtn) { 35 const currency = bricksToCurrency(parseInt(purchaseBtn.getAttribute('data-price')!), "USD"); 36 37 if (currency) { 38 const spanTag = document.createElement('span'); 39 spanTag.classList.add('text-muted'); 40 spanTag.style.fontSize = '0.7rem'; 41 spanTag.style.fontWeight = 'lighter'; 42 spanTag.innerText = ` (${currency})`; 43 price.appendChild(spanTag); 44 }; 45 }; 46 } 47 48 const resellers = document.getElementById('resellers-container'); 49 if (resellers) { 50 for (const reseller of resellers.children) addPrice(reseller as HTMLElement); 51 52 const mutations = new MutationObserver((mutations) => { 53 for (const record of mutations) { 54 for (const node of record.addedNodes) { 55 addPrice(node as HTMLElement); 56 }; 57 }; 58 }); 59 60 mutations.observe(resellers, { childList: true }); 61 }; 62}; 63 64/** 65 * Adds a button below the item thumbnail preview allowing the user to preview their avatar with the item they are looking at equipped. 66 * @param user The public-facing details of the authenticated user. 67 */ 68export function tryOn(user: userDetails) { 69 const itemId = window.location.pathname.split('/')[2] 70 const favoriteBtn = document.querySelector('button[onclick^="toggleFavorite"]')!; 71 72 const button = document.createElement('button'); 73 button.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'mt-2'); 74 button.style.width = '50px'; 75 button.innerHTML = '<img src="' + browser.runtime.getURL("/svgs/vial.svg") + '" width="15" height="15">'; 76 77 const modal = document.createElement('dialog'); 78 modal.classList.add('polyplus-modal'); 79 Object.assign(modal.style, { 80 width: '450px', 81 height: '350px', 82 border: '1px solid #484848', 83 backgroundColor: '#181818', 84 borderRadius: '20px', 85 overflow: 'hidden' 86 }); 87 88 modal.innerHTML = ` 89 <div class="row text-muted mb-2" style="font-size: 0.8rem;"> 90 <div class="col"> 91 <h5 class="mb-0" style="color: #fff;">Poly+ Item Preview</h5> 92 Try this item on your avatar before purchasing it! 93 </div> 94 <div class="col-md-2"> 95 <button class="btn btn-info w-100 mx-auto" onclick="this.parentElement.parentElement.parentElement.close();">X</button> 96 </div> 97 </div> 98 </div> 99 <div class="modal-body"></div> 100 `; 101 102 document.body.prepend(modal); 103 favoriteBtn.parentElement!.appendChild(button); 104 105 const loadFrame = async (source: apiTypes.avatarApiSchema) => { 106 console.info('[Poly+] Loading avatar preview with avatar data: ', source); 107 for (const [key, value] of Object.entries(source.colors)) { 108 source.colors[key as keyof typeof source.colors] = "#" + value; 109 }; 110 111 const avatar: { [key: string]: any } = { 112 useCharacter: true, 113 items: [] as Array<number>, 114 headColor: source.colors.head, 115 torsoColor: source.colors.torso, 116 leftArmColor: source.colors.leftArm, 117 rightArmColor: source.colors.rightArm, 118 leftLegColor: source.colors.leftLeg, 119 rightLegColor: source.colors.rightLeg, 120 }; 121 122 const itemInfo: apiTypes.itemApiSchema = (await (await fetch(config.api.urls.public + "store/" + itemId)).json()); 123 for (const item of [ 124 ...source.assets.filter((item: any) => (item.type != itemInfo.type || itemInfo.type == "hat")), 125 { 126 id: itemInfo.id, 127 type: itemInfo.type 128 } 129 ]) { 130 if (item.type == "hat" || item.type == "tool") { 131 const mesh: apiTypes.meshApiSchema = (await (await fetch(config.api.urls.public + "assets/serve-mesh/" + item.id)).json()); 132 if (mesh.success) { 133 if (item.type == "hat") { 134 avatar.items.push(mesh.url) 135 } else { 136 avatar.tool = mesh.url; 137 }; 138 }; 139 } else { 140 const texture: apiTypes.textureApiSchema = (await (await fetch(config.api.urls.public + "assets/serve/" + item.id + "/Asset")).json()); 141 if (texture.success) avatar[item.type] = texture.url; 142 }; 143 }; 144 145 const frame = document.createElement('iframe'); 146 Object.assign(frame.style, { 147 width: '100%', 148 height: 'auto', 149 aspectRatio: '1', 150 borderRadius: '20px', 151 background: '#1e1e1e' 152 }) 153 frame.src = config.api.urls.itemview + btoa(encodeURIComponent(JSON.stringify(avatar))); 154 155 modal.getElementsByClassName('modal-body')[0].appendChild(frame); 156 }; 157 158 button.addEventListener('click', async () => { 159 if (!modal.getElementsByTagName('iframe')[0]) { 160 button.innerHTML = ` 161 <span class="spinner-grow spinner-grow-sm" aria-hidden="true"></span> 162 <span class="visually-hidden" role="status">Loading...</span> 163 `; 164 165 const avatar = await user.getAvatar(); 166 if (avatar == "disabled") { 167 console.error('[Poly+] API is disabled, cancelling try-on items loading..'); 168 169 const modalBody = modal.getElementsByClassName('modal-body')[0] as HTMLDivElement; 170 modalBody.style.marginTop = '50px'; 171 172 modalBody.innerHTML = ` 173 <div class="text-center p-2"> 174 <img src="${ browser.runtime.getURL('/svgs/error.svg') }" width="100" height="100"> 175 <p class="text-muted mb-0">Sorry! This feature is currently unavailable. Please check back later!</p> 176 </div> 177 `; 178 179 modal.showModal(); 180 button.innerHTML = '<img src="' + browser.runtime.getURL("/svgs/vial.svg") + '" width="15" height="15">'; 181 return; 182 }; 183 184 await loadFrame(avatar); 185 button.innerHTML = '<img src="' + browser.runtime.getURL("/svgs/vial.svg") + '" width="15" height="15">'; 186 }; 187 188 modal.showModal(); 189 }); 190};