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