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