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 { preferences, _favoritedPlaces, _bestFriends } from "@/utils/storage";
2import { pullCache } from "@/utils/utilities";
3import * as api from "@/utils/api";
4import * as apiTypes from "@/utils/api/types";
5import errorIcon from "@/assets/error.svg";
6
7export default defineContentScript({
8 matches: ['https://polytoria.com/', 'https://polytoria.com/home'],
9 main() {
10 preferences.getPreferences()
11 .then((values) => {
12 if (values.favoritedPlaces.enabled) favoritedPlaces();
13 if (values.bestFriends.enabled) bestFriends();
14 if (values.irlBrickPrice.enabled) irlBrickPrice();
15 });
16 }
17});
18
19function favoritedPlaces() {
20 _favoritedPlaces.getValue()
21 .then(async (places) => {
22 const container = document.createElement('div');
23 container.innerHTML = `
24 <div class="row reqFadeAnim px-2 px-lg-0">
25 <div class="col">
26 <h6 class="dash-ctitle2">Jump right back into your favorite places</h6>
27 <h5 class="dash-ctitle">Favorited Places</h5>
28 </div>
29 </div>
30 <div class="card card-dash mcard mb-3">
31 <div class="card-body p-0 m-1 scrollFadeContainer">
32 <div class="text-center p-5">
33 <div class="spinner-border text-muted" role="status">
34 <span class="visually-hidden">Loading...</span>
35 </div>
36 </div>
37 </div>
38 </div>
39 `;
40
41 const card: HTMLElement = container.getElementsByClassName('scrollFadeContainer')[0] as HTMLElement;
42 const column = document.getElementsByClassName('col-lg-8')[0];
43
44 if (document.getElementsByClassName('home-event-container')[0] === undefined) {
45 column.insertBefore(container, column.children[0]);
46 } else {
47 column.insertBefore(container, column.children[1]);
48 };
49
50 const placeData: Array<apiTypes.placeApiSchema> | "disabled" = await pullCache(
51 'favoritedPlaces',
52 async () => await api.batch('public', 'places/', places),
53 300000, // 5 minutes
54 false
55 );
56
57 if (placeData == "disabled") {
58 console.error('[Poly+] API is disabled, cancelling favorited places loading..');
59 card.innerHTML = `
60 <div class="text-center p-2">
61 <img src="${ errorIcon }" width="100" height="100">
62 <p class="text-muted mb-0">Sorry! This feature is currently unavailable. Please check back later!</p>
63 </div>
64 `;
65
66 return;
67 };
68
69 for (let i = 0; i < places.length; i++) {
70 const id = places.toSorted((a, b) => parseInt(b) - parseInt(a))[i]
71 const details = placeData[parseInt(id)]
72 if (!details) {
73 console.warn("[Poly+] Missing cached place data for ID " + id);
74 continue;
75 }
76
77 const scrollCard = document.createElement('a')
78 scrollCard.classList.value = 'd-none'
79 scrollCard.href = '/places/' + id
80 scrollCard.innerHTML = `
81 <div class="scrollFade card me-2 place-card force-desktop text-center mb-2" style="opacity: 1;">
82 <div class="card-body">
83 <div class="ratings-header">
84 <img src="${details.thumbnail}" class="place-card-image" style="position: relative;">
85 <div class="p+pinned_games_playing" style="position: absolute;background: linear-gradient(to bottom, #000000f7, transparent, transparent, transparent);width: 100%;height: 100%;top: 0;left: 0;border-radius: 15px;padding-top: 12px;color: gray;font-size: 0.8rem;">
86 <i class="fa-duotone fa-users"></i>
87 <span>
88 ${details.playing}
89 Playing
90 </span>
91 </div>
92 </div>
93 <div>
94 <div class="mt-2 mb-1 place-card-title">
95 ${details.name}
96 </div>
97 </div>
98 </div>
99 </div>
100 `
101
102 if (!details.isActive) {
103 const PlayerCountText = scrollCard.getElementsByClassName('p+pinned_games_playing')[0];
104 PlayerCountText.children[0].classList.value = 'text-warning fa-duotone fa-lock';
105 PlayerCountText.children[1].remove();
106 }
107
108 card.appendChild(scrollCard);
109 }
110
111 // Remove Loading Spinner
112 card.children[0].remove();
113 card.classList.add('d-flex');
114 Array.from(card.children).forEach((place) => { place.classList.remove('d-none') });
115 });
116};
117
118function bestFriends() {
119 const friendsRow = document.querySelector('.card:has(.friendsPopup) .d-flex')!;
120
121 const createHeadshot = async function(id: string) {
122 const user: apiTypes.userApiSchema = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json());
123 const headshot = document.createElement('div');
124
125 // ? surely a better way to do this but who cares
126 headshot.classList.add('friend-circle');
127 headshot.setAttribute('data-user-id', id.toString());
128 headshot.setAttribute('data-username', user.username);
129 headshot.setAttribute('data-is-online', 'false');
130 headshot.setAttribute('data-location', 'offline');
131
132 headshot.innerHTML = `
133 <img width="90" height="auto" src="${user.thumbnail.icon}" alt="${user.username}" class="img-fluid rounded-circle border border-2 ">
134 <div class="friend-name text-truncate mt-1">
135 <div style="font-size: 0.5rem; line-height: 0.5rem; display: inline-block;">
136 <span class="text-muted">
137 <i class="fas fa-dot-circle"></i>
138 </span>
139 </div>
140 ${user.username}
141 </div>
142 `;
143
144 friendsRow.prepend(headshot);
145 return headshot;
146 };
147
148 _bestFriends.getValue()
149 .then(async (friends) => {
150 const userData = await pullCache(
151 'bestFriends',
152 async () => await api.batch('public', 'users/', friends),
153 300000, // 5 minutes
154 false
155 );
156
157 if (userData == "disabled") {
158 console.error('[Poly+] API is disabled, cancelling best friends loading..');
159 return;
160 };
161
162 for (const id of friends) {
163 if (!userData[id]) {
164 console.warn("[Poly+] Missing cached user data for ID " + id);
165 continue;
166 };
167
168 let headshot = document.getElementById('friend-' + id);
169 if (!headshot) headshot = await createHeadshot(id);
170 friendsRow.prepend(headshot, friendsRow.children[0]);
171 };
172 });
173};
174
175function irlBrickPrice() {
176 const trendingItems = Array.from(document.querySelectorAll('a[href^="/store"]:has(.place-card)'));
177
178 for (const item of trendingItems) {
179 const priceTag = item.getElementsByClassName('text-success')[0];
180 const currency = bricksToCurrency(parseInt(priceTag.textContent!), "USD");
181
182 if (currency) {
183 const spanTag = document.createElement('span');
184 spanTag.classList.add('text-muted');
185 spanTag.style.fontSize = '0.7rem';
186 spanTag.style.fontWeight = 'lighter';
187 spanTag.innerText = ` (${currency})`;
188 priceTag.appendChild(spanTag);
189 };
190 }
191};