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}