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