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";
4
5export default defineContentScript({
6 matches: ['https://polytoria.com/', 'https://polytoria.com/home'],
7 main() {
8 preferences.getValue()
9 .then((values) => {
10 if (values.favoritedPlaces.enabled) favoritedPlaces();
11 if (values.bestFriends.enabled) bestFriends();
12 });
13 }
14});
15
16function favoritedPlaces() {
17 _favoritedPlaces.getValue()
18 .then(async (places) => {
19 const container = document.createElement('div');
20 container.innerHTML = `
21 <div class="row reqFadeAnim px-2 px-lg-0">
22 <div class="col">
23 <h6 class="dash-ctitle2">Jump right back into your favorite places</h6>
24 <h5 class="dash-ctitle">Favorited Places</h5>
25 </div>
26 </div>
27 <div class="card card-dash mcard mb-3">
28 <div class="card-body p-0 m-1 scrollFadeContainer">
29 <div class="text-center p-5">
30 <div class="spinner-border text-muted" role="status">
31 <span class="visually-hidden">Loading...</span>
32 </div>
33 </div>
34 </div>
35 </div>
36 `;
37
38 const column = document.getElementsByClassName('col-lg-8')[0];
39 if (document.getElementsByClassName('home-event-container')[0] === undefined) {
40 column.insertBefore(container, column.children[0]);
41 } else {
42 column.insertBefore(container, column.children[1]);
43 }
44
45 const placeData: Array<placeApiSchema> = await pullCache('favoritedPlaces', async () => {
46 // API is not viable right now, static data used in it's place for now.
47 return {
48 "9656": {"id":9656,"name":"The Wayland Bridge","description":"Listen up troops,\r\n\r\nOn this battlefield your mission is to capture the enemy flag and perform as many casualties to the enemy team. The first team to reach 5 captures will claim victory. The high order will reward you for your confirmed kills, use this wisely to upgrade your arsenal.\r\n\r\nGood luck soldier.","creator":{"type":"user","id":1,"name":"Polytoria","thumbnail":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e-icon.png"},"thumbnail":"https://c0.ptacdn.com/places/icons/WxomBAbMNNNnnAkKxuD11Snnoz2C26wW.png","genre":"fighting","maxPlayers":24,"isActive":false,"isToolsEnabled":true,"isCopyable":false,"visits":10205,"uniqueVisits":799,"playing":0,"rating":{"likes":149,"dislikes":49,"percent":"75%"},"accessType":"everyone","accessPrice":null,"createdAt":"2024-06-14T07:48:36.720+00:00","updatedAt":"2024-07-14T09:07:53.198+00:00"}
49 };
50
51 /*
52 const res: any = {}
53 for (let id of places as number[]) {
54 const info = (await (await fetch('https://api.polytoria.com/v1/places/' + id)).json())
55 res[id] = info
56 }
57 return res;
58 */
59 }, 300000, true);
60
61 const card = container.getElementsByClassName('scrollFadeContainer')[0]
62 for (let i = 0; i < places.length; i++) {
63 const id = places.toSorted((a, b) => parseInt(b) - parseInt(a))[i]
64 const details = placeData[parseInt(id)]
65 if (!details) {
66 console.warn("[Poly+] Missing cached place data for ID " + id);
67 continue;
68 }
69
70 const scrollCard = document.createElement('a')
71 scrollCard.classList.value = 'd-none'
72 scrollCard.href = '/places/' + id
73 scrollCard.innerHTML = `
74 <div class="scrollFade card me-2 place-card force-desktop text-center mb-2" style="opacity: 1;">
75 <div class="card-body">
76 <div class="ratings-header">
77 <img src="${details.thumbnail}" class="place-card-image" style="position: relative;">
78 <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;">
79 <i class="fa-duotone fa-users"></i>
80 <span>
81 ${details.playing}
82 Playing
83 </span>
84 </div>
85 </div>
86 <div>
87 <div class="mt-2 mb-1 place-card-title">
88 ${details.name}
89 </div>
90 </div>
91 </div>
92 </div>
93 `
94
95 if (!details.isActive) {
96 const PlayerCountText = scrollCard.getElementsByClassName('p+pinned_games_playing')[0];
97 PlayerCountText.children[0].classList.value = 'text-warning fa-duotone fa-lock';
98 PlayerCountText.children[1].remove();
99 }
100
101 card.appendChild(scrollCard);
102 }
103 card.children[0].remove();
104 card.classList.add('d-flex');
105 Array.from(card.children).forEach((place) => {place.classList.remove('d-none')});
106 });
107}
108
109function bestFriends() {
110 const friendsRow = document.querySelector('.card:has(.friendsPopup) .d-flex')!;
111
112 const createHeadshot = async function(id: string) {
113 const user: userApiSchema = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json());
114 const headshot = document.createElement('div');
115
116 // ? surely a better way to do this but who cares
117 headshot.classList.add('friend-circle');
118 headshot.setAttribute('data-user-id', id.toString());
119 headshot.setAttribute('data-username', user.username);
120 headshot.setAttribute('data-is-online', 'false');
121 headshot.setAttribute('data-location', 'offline');
122
123 headshot.innerHTML = `
124 <img width="90" height="auto" src="${user.thumbnail.icon}" alt="${user.username}" class="img-fluid rounded-circle border border-2 ">
125 <div class="friend-name text-truncate mt-1">
126 <div style="font-size: 0.5rem; line-height: 0.5rem; display: inline-block;">
127 <span class="text-muted">
128 <i class="fas fa-dot-circle"></i>
129 </span>
130 </div>
131 ${user.username}
132 </div>
133 `;
134
135 friendsRow.prepend(headshot);
136 return headshot;
137 };
138
139 _bestFriends.getValue()
140 .then(async (friends) => {
141 const userData = await pullCache('bestFriends', async () => {
142 // API is not viable right now, static data used in it's place for now.
143 return {
144 "1": {"id":1,"username":"Polytoria","description":"Welcome to the Polytoria profile!\r\nFor inquiries or support, please send a message to one of our staff members. Messages sent to this account will not be read!","signature":"Hello!","thumbnail":{"avatar":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e.png","icon":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e-icon.png"},"playing":null,"netWorth":3991,"placeVisits":20461,"profileViews":2567,"forumPosts":17,"assetSales":64172,"membershipType":"plusDeluxe","isStaff":true,"registeredAt":"2019-04-14T12:29:52.000+00:00","lastSeenAt":"2024-11-21T11:34:14.119+00:00"}
145 };
146
147 /*
148 const res: any = {}
149 for (let id of friends as number[]) {
150 const info = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json())
151 res[id] = info
152 }
153 return res;
154 */
155 }, 300000, true);
156
157 for (const id of friends) {
158 let headshot = document.getElementById('friend-' + id);
159 if (!headshot) headshot = await createHeadshot(id);
160 friendsRow.prepend(headshot, friendsRow.children[0]);
161 };
162 });
163}