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