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