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