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