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 { _bestFriends, _favoritedPlaces, preferences } 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.enabled.includes("favoritedPlaces")) favoritedPlaces();
13 if (values.enabled.includes("bestFriends")) bestFriends();
14 if (values.enabled.includes("irlBrickPrice")) 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(
42 "scrollFadeContainer",
43 )[0] as HTMLElement;
44 const column = document.getElementsByClassName("col-lg-8")[0];
45
46 if (
47 document.getElementsByClassName("home-event-container")[0] === undefined
48 ) {
49 column.insertBefore(container, column.children[0]);
50 } else {
51 column.insertBefore(container, column.children[1]);
52 }
53
54 const placeData: Array<apiTypes.placeApiSchema> | "disabled" =
55 await pullCache(
56 "favoritedPlaces",
57 async () => await api.batch("public", "places/", places),
58 300000, // 5 minutes
59 false,
60 );
61
62 if (placeData == "disabled") {
63 console.error(
64 "[Poly+] API is disabled, cancelling favorited places loading..",
65 );
66 card.innerHTML = `
67 <div class="text-center p-2">
68 <img src="${errorIcon}" width="100" height="100">
69 <p class="text-muted mb-0">Sorry! This feature is currently unavailable. Please check back later!</p>
70 </div>
71 `;
72
73 return;
74 }
75
76 for (let i = 0; i < places.length; i++) {
77 const id = places.toSorted((a, b) => parseInt(b) - parseInt(a))[i];
78 const details = placeData[parseInt(id)];
79 if (!details) {
80 console.warn("[Poly+] Missing cached place data for ID " + id);
81 continue;
82 }
83
84 const scrollCard = document.createElement("a");
85 scrollCard.classList.value = "d-none";
86 scrollCard.href = "/places/" + id;
87 scrollCard.innerHTML = `
88 <div class="scrollFade card me-2 place-card force-desktop text-center mb-2" style="opacity: 1;">
89 <div class="card-body">
90 <div class="ratings-header">
91 <img src="${details.thumbnail}" class="place-card-image" style="position: relative;">
92 <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;">
93 <i class="fa-duotone fa-users"></i>
94 <span>
95 ${details.playing}
96 Playing
97 </span>
98 </div>
99 </div>
100 <div>
101 <div class="mt-2 mb-1 place-card-title">
102 ${details.name}
103 </div>
104 </div>
105 </div>
106 </div>
107 `;
108
109 if (!details.isActive) {
110 const PlayerCountText =
111 scrollCard.getElementsByClassName("p+pinned_games_playing")[0];
112 PlayerCountText.children[0].classList.value =
113 "text-warning fa-duotone fa-lock";
114 PlayerCountText.children[1].remove();
115 }
116
117 card.appendChild(scrollCard);
118 }
119
120 // Remove Loading Spinner
121 card.children[0].remove();
122 card.classList.add("d-flex");
123 Array.from(card.children).forEach((place) => {
124 place.classList.remove("d-none");
125 });
126 });
127}
128
129function bestFriends() {
130 const friendsRow = document.querySelector(
131 ".card:has(.friendsPopup) .d-flex",
132 )!;
133
134 const createHeadshot = async function (id: string) {
135 const user: apiTypes.userApiSchema =
136 await (await fetch("https://api.polytoria.com/v1/users/" + id)).json();
137 const headshot = document.createElement("div");
138
139 // ? surely a better way to do this but who cares
140 headshot.classList.add("friend-circle");
141 headshot.setAttribute("data-user-id", id.toString());
142 headshot.setAttribute("data-username", user.username);
143 headshot.setAttribute("data-is-online", "false");
144 headshot.setAttribute("data-location", "offline");
145
146 headshot.innerHTML = `
147 <img width="90" height="auto" src="${user.thumbnail.icon}" alt="${user.username}" class="img-fluid rounded-circle border border-2 ">
148 <div class="friend-name text-truncate mt-1">
149 <div style="font-size: 0.5rem; line-height: 0.5rem; display: inline-block;">
150 <span class="text-muted">
151 <i class="fas fa-dot-circle"></i>
152 </span>
153 </div>
154 ${user.username}
155 </div>
156 `;
157
158 friendsRow.prepend(headshot);
159 return headshot;
160 };
161
162 _bestFriends.getValue()
163 .then(async (friends) => {
164 const userData = await pullCache(
165 "bestFriends",
166 async () => await api.batch("public", "users/", friends),
167 300000, // 5 minutes
168 false,
169 );
170
171 if (userData == "disabled") {
172 console.error(
173 "[Poly+] API is disabled, cancelling best friends loading..",
174 );
175 return;
176 }
177
178 for (const id of friends) {
179 if (!userData[id]) {
180 console.warn("[Poly+] Missing cached user data for ID " + id);
181 continue;
182 }
183
184 let headshot = document.getElementById("friend-" + id);
185 if (!headshot) headshot = await createHeadshot(id);
186 friendsRow.prepend(headshot, friendsRow.children[0]);
187 }
188 });
189}
190
191function irlBrickPrice() {
192 const trendingItems = Array.from(
193 document.querySelectorAll('a[href^="/store"]:has(.place-card)'),
194 );
195
196 for (const item of trendingItems) {
197 const priceTag = item.getElementsByClassName("text-success")[0];
198 const currency = bricksToCurrency(parseInt(priceTag.textContent!), "USD");
199
200 if (currency) {
201 const spanTag = document.createElement("span");
202 spanTag.classList.add("text-muted");
203 spanTag.style.fontSize = "0.7rem";
204 spanTag.style.fontWeight = "lighter";
205 spanTag.innerText = ` (${currency})`;
206 priceTag.appendChild(spanTag);
207 }
208 }
209}